@noble/post-quantum 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +102 -89
- package/_crystals.d.ts +8 -3
- package/_crystals.js +37 -9
- package/falcon.d.ts +0 -1
- package/falcon.js +82 -54
- package/hybrid.d.ts +5 -16
- package/hybrid.js +121 -51
- package/index.d.ts +0 -1
- package/index.js +0 -1
- package/ml-dsa.d.ts +28 -2
- package/ml-dsa.js +62 -15
- package/ml-kem.d.ts +45 -4
- package/ml-kem.js +132 -41
- package/package.json +9 -17
- package/slh-dsa.d.ts +23 -3
- package/slh-dsa.js +93 -49
- package/src/_crystals.ts +44 -10
- package/src/falcon.ts +80 -52
- package/src/hybrid.ts +115 -50
- package/src/ml-dsa.ts +71 -19
- package/src/ml-kem.ts +174 -42
- package/src/slh-dsa.ts +105 -56
- package/src/utils.ts +71 -15
- package/utils.d.ts +18 -2
- package/utils.js +63 -17
- 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/slh-dsa.js
CHANGED
|
@@ -28,9 +28,10 @@
|
|
|
28
28
|
*/
|
|
29
29
|
/*! noble-post-quantum - MIT License (c) 2024 Paul Miller (paulmillr.com) */
|
|
30
30
|
import { hmac } from '@noble/hashes/hmac.js';
|
|
31
|
+
import { bytesToNumberBE, numberToBytesBE } from '@noble/curves/utils.js';
|
|
31
32
|
import { sha256, sha512 } from '@noble/hashes/sha2.js';
|
|
32
33
|
import { shake256 } from '@noble/hashes/sha3.js';
|
|
33
|
-
import {
|
|
34
|
+
import { concatBytes, createView } from '@noble/hashes/utils.js';
|
|
34
35
|
import { abytes, checkHash, cleanBytes, copyBytes, equalBytes, getMask, getMessage, getMessagePrehash, randomBytes, splitCoder, validateSigOpts, validateVerOpts, vecCoder, } from "./utils.js";
|
|
35
36
|
/** Winternitz signature params. */
|
|
36
37
|
/**
|
|
@@ -61,19 +62,6 @@ const AddressType = {
|
|
|
61
62
|
WOTSPRF: 5,
|
|
62
63
|
FORSPRF: 6,
|
|
63
64
|
};
|
|
64
|
-
function hexToNumber(hex) {
|
|
65
|
-
if (typeof hex !== 'string')
|
|
66
|
-
throw new Error('hex string expected, got ' + typeof hex);
|
|
67
|
-
return BigInt(hex === '' ? '0' : '0x' + hex); // Big Endian
|
|
68
|
-
}
|
|
69
|
-
// BE: Big Endian, LE: Little Endian. This is the local FIPS 205 `toInt(...)` equivalent.
|
|
70
|
-
function bytesToNumberBE(bytes) {
|
|
71
|
-
return hexToNumber(bytesToHex(bytes));
|
|
72
|
-
}
|
|
73
|
-
// Local in-range FIPS 205 `toByte(x, n)` equivalent; callers must keep `n < 256^len`.
|
|
74
|
-
function numberToBytesBE(n, len) {
|
|
75
|
-
return hexToBytes(n.toString(16).padStart(len * 2, '0'));
|
|
76
|
-
}
|
|
77
65
|
// Local FIPS 205 Algorithm 4 `base_2^b(...)` implementation. Bits are consumed in big-endian
|
|
78
66
|
// order within each input byte, and callers must provide at least `ceil(outLen * b / 8)` bytes;
|
|
79
67
|
// short inputs are not rejected and would zero-extend implicitly.
|
|
@@ -92,8 +80,11 @@ const base2b = (outLen, b) => {
|
|
|
92
80
|
return baseB;
|
|
93
81
|
};
|
|
94
82
|
};
|
|
83
|
+
const _1n = /* @__PURE__ */ BigInt(1);
|
|
84
|
+
const _8n = /* @__PURE__ */ BigInt(8);
|
|
85
|
+
const _0xffn = /* @__PURE__ */ BigInt(0xff);
|
|
95
86
|
function getMaskBig(bits) {
|
|
96
|
-
return (
|
|
87
|
+
return (_1n << BigInt(bits)) - _1n; // 4 -> 0b1111
|
|
97
88
|
}
|
|
98
89
|
/** One parameter/hash instantiation of the public SLH-DSA API.
|
|
99
90
|
* `keygen(seed)` is a deterministic 3N-byte library hook around the internal keygen flow,
|
|
@@ -139,7 +130,6 @@ function gen(opts, hashOpts_) {
|
|
|
139
130
|
const setAddr = (opts, addr = new Uint8Array(ADDR_BYTES)) => {
|
|
140
131
|
const { type, height, tree, layer, index, chain, hash, keypair } = opts;
|
|
141
132
|
const { subtreeAddr, keypairAddr } = opts;
|
|
142
|
-
const v = createView(addr);
|
|
143
133
|
if (height !== undefined)
|
|
144
134
|
addr[OFFSET_CHAIN_ADDR] = height;
|
|
145
135
|
if (layer !== undefined)
|
|
@@ -150,12 +140,21 @@ function gen(opts, hashOpts_) {
|
|
|
150
140
|
addr[OFFSET_CHAIN_ADDR] = chain;
|
|
151
141
|
if (hash !== undefined)
|
|
152
142
|
addr[OFFSET_HASH_ADDR] = hash;
|
|
153
|
-
|
|
154
|
-
|
|
143
|
+
// Manual big-endian writes: setAddr runs in the innermost WOTS/tree loops, and creating a
|
|
144
|
+
// DataView per call was a measurable share of sign() time.
|
|
145
|
+
if (index !== undefined) {
|
|
146
|
+
addr[OFFSET_TREE_INDEX + 0] = index >>> 24;
|
|
147
|
+
addr[OFFSET_TREE_INDEX + 1] = index >>> 16;
|
|
148
|
+
addr[OFFSET_TREE_INDEX + 2] = index >>> 8;
|
|
149
|
+
addr[OFFSET_TREE_INDEX + 3] = index;
|
|
150
|
+
}
|
|
155
151
|
if (subtreeAddr)
|
|
156
152
|
addr.set(subtreeAddr.subarray(0, OFFSET_TREE + 8));
|
|
157
|
-
if (tree !== undefined)
|
|
158
|
-
|
|
153
|
+
if (tree !== undefined) {
|
|
154
|
+
let t = tree;
|
|
155
|
+
for (let i = 7; i >= 0; i--, t >>= _8n)
|
|
156
|
+
addr[OFFSET_TREE + i] = Number(t & _0xffn);
|
|
157
|
+
}
|
|
159
158
|
if (keypair !== undefined) {
|
|
160
159
|
addr[OFFSET_KP_ADDR1] = keypair;
|
|
161
160
|
if (TREE_HEIGHT > 8)
|
|
@@ -209,10 +208,12 @@ function gen(opts, hashOpts_) {
|
|
|
209
208
|
const maxIdx = (1 << height) - 1;
|
|
210
209
|
const stack = new Uint8Array(height * N);
|
|
211
210
|
const authPath = new Uint8Array(height * N);
|
|
211
|
+
// One node buffer per treehash call (not per leaf): both halves are fully overwritten at
|
|
212
|
+
// each use, and the returned root aliases cur1, which is never reused after return.
|
|
213
|
+
const current = new Uint8Array(2 * N);
|
|
214
|
+
const cur0 = current.subarray(0, N);
|
|
215
|
+
const cur1 = current.subarray(N);
|
|
212
216
|
for (let idx = 0;; idx++) {
|
|
213
|
-
const current = new Uint8Array(2 * N);
|
|
214
|
-
const cur0 = current.subarray(0, N);
|
|
215
|
-
const cur1 = current.subarray(N);
|
|
216
217
|
const addrOffset = idx + idxOffset;
|
|
217
218
|
cur1.set(leafFn(leafIdx, addrOffset, rawContext, info));
|
|
218
219
|
let h = 0;
|
|
@@ -394,7 +395,9 @@ function gen(opts, hashOpts_) {
|
|
|
394
395
|
height: 0,
|
|
395
396
|
index: indices[i] + idxOffset,
|
|
396
397
|
}, forsTreeAddr);
|
|
397
|
-
|
|
398
|
+
// Copy: PRFaddr returns a per-context scratch view, and this value is retained in
|
|
399
|
+
// `fors` across the many PRFaddr calls inside forsTreehash below.
|
|
400
|
+
const prf = copyBytes(context.PRFaddr(forsTreeAddr));
|
|
398
401
|
setAddr({ type: AddressType.FORSTREE }, forsTreeAddr);
|
|
399
402
|
const { root, authPath } = forsTreehash(context, indices[i], idxOffset, forsTreeAddr, forsLeaf);
|
|
400
403
|
roots.push(root);
|
|
@@ -404,7 +407,9 @@ function gen(opts, hashOpts_) {
|
|
|
404
407
|
type: AddressType.FORSPK,
|
|
405
408
|
keypairAddr: wotsAddr,
|
|
406
409
|
});
|
|
407
|
-
|
|
410
|
+
// Copy: thashN returns a per-context scratch view, and `root` lives across every hash
|
|
411
|
+
// call in the hypertree loop below (it is also mutated via root.set).
|
|
412
|
+
const root = copyBytes(context.thashN(K, concatBytes(...roots), forsPkAddr));
|
|
408
413
|
// WOTS signatures
|
|
409
414
|
const treeAddr = setAddr({ type: AddressType.HASHTREE });
|
|
410
415
|
const wots = [];
|
|
@@ -424,10 +429,14 @@ function gen(opts, hashOpts_) {
|
|
|
424
429
|
},
|
|
425
430
|
verify: (sig, msg, publicKey) => {
|
|
426
431
|
const [pkSeed, pubRoot] = publicCoder.decode(publicKey);
|
|
427
|
-
const [random, forsVec, wotsVec] = sigCoder.decode(sig);
|
|
428
432
|
const pk = publicKey;
|
|
433
|
+
// FIPS 205 Algorithm 20 step 1: wrong-length signatures return false instead of throwing
|
|
434
|
+
// (same as ml-dsa). Must run before sigCoder.decode, which throws on length mismatch.
|
|
435
|
+
// Preserve TypeError for non-byte API arguments before treating byte lengths as invalid.
|
|
436
|
+
abytes(sig, undefined, 'signature');
|
|
429
437
|
if (sig.length !== sigCoder.bytesLen)
|
|
430
438
|
return false;
|
|
439
|
+
const [random, forsVec, wotsVec] = sigCoder.decode(sig);
|
|
431
440
|
const context = getContext(pkSeed);
|
|
432
441
|
let { tree, leafIdx, md } = hashMessage(random, pk, msg, context);
|
|
433
442
|
const wotsAddr = setAddr({
|
|
@@ -447,14 +456,16 @@ function gen(opts, hashOpts_) {
|
|
|
447
456
|
const idxOffset = i << A;
|
|
448
457
|
setAddr({ height: 0, index: indices[i] + idxOffset }, forsTreeAddr);
|
|
449
458
|
const leaf = context.thash1(prf, forsTreeAddr);
|
|
450
|
-
//
|
|
451
|
-
|
|
459
|
+
// Copy: computeRoot returns a thashN scratch view, and roots are retained across the
|
|
460
|
+
// remaining FORS iterations (computeRoot itself copies `leaf` before hashing).
|
|
461
|
+
roots.push(copyBytes(computeRoot(leaf, indices[i], idxOffset, authPath, A, context, forsTreeAddr)));
|
|
452
462
|
}
|
|
453
463
|
const forsPkAddr = setAddr({
|
|
454
464
|
type: AddressType.FORSPK,
|
|
455
465
|
keypairAddr: wotsAddr,
|
|
456
466
|
});
|
|
457
|
-
|
|
467
|
+
// Copy: `root` must survive the thash1/thashN calls of the WOTS chain loop below.
|
|
468
|
+
let root = copyBytes(context.thashN(K, concatBytes(...roots), forsPkAddr)); // root = thash()
|
|
458
469
|
// WOTS signature
|
|
459
470
|
const treeAddr = setAddr({ type: AddressType.HASHTREE });
|
|
460
471
|
const wotsPkAddr = setAddr({ type: AddressType.WOTSPK });
|
|
@@ -477,7 +488,8 @@ function gen(opts, hashOpts_) {
|
|
|
477
488
|
}
|
|
478
489
|
}
|
|
479
490
|
const leaf = context.thashN(WOTS_LEN, wotsPk, wotsPkAddr);
|
|
480
|
-
root
|
|
491
|
+
// Copy: `root` is read by chainLengths / equalBytes after later hash calls.
|
|
492
|
+
root = copyBytes(computeRoot(leaf, leafIdx, 0, sigAuth, TREE_HEIGHT, context, treeAddr));
|
|
481
493
|
leafIdx = Number(tree & getMaskBig(TREE_HEIGHT));
|
|
482
494
|
}
|
|
483
495
|
return equalBytes(root, pubRoot);
|
|
@@ -533,21 +545,26 @@ const genShake = () => (opts) => (pubSeed, skSeed) => {
|
|
|
533
545
|
// for each address-bound call instead of reabsorbing the same seed every time.
|
|
534
546
|
const h0 = shake256.create({}).update(pubSeed);
|
|
535
547
|
const h0tmp = h0.clone();
|
|
548
|
+
// Per-context output scratch: thash1/thashN/PRFaddr return these buffers directly, so
|
|
549
|
+
// callers must consume or copy a result before the next call on the same lane.
|
|
550
|
+
const thashOut = new Uint8Array(N);
|
|
551
|
+
const prfOut = new Uint8Array(N);
|
|
536
552
|
const thash = (blocks, input, addr) => {
|
|
537
553
|
stats.thash++;
|
|
538
|
-
|
|
539
|
-
|
|
554
|
+
const len = blocks * N;
|
|
555
|
+
h0._cloneInto(h0tmp)
|
|
540
556
|
.update(addr)
|
|
541
|
-
.update(input.subarray(0,
|
|
542
|
-
.
|
|
557
|
+
.update(input.length === len ? input : input.subarray(0, len))
|
|
558
|
+
.xofInto(thashOut);
|
|
559
|
+
return thashOut;
|
|
543
560
|
};
|
|
544
561
|
return {
|
|
545
562
|
PRFaddr: (addr) => {
|
|
546
563
|
if (!skSeed)
|
|
547
564
|
throw new Error('no sk seed');
|
|
548
565
|
stats.prf++;
|
|
549
|
-
|
|
550
|
-
return
|
|
566
|
+
h0._cloneInto(h0tmp).update(addr).update(skSeed).xofInto(prfOut);
|
|
567
|
+
return prfOut;
|
|
551
568
|
},
|
|
552
569
|
PRFmsg: (skPRF, random, msg) => {
|
|
553
570
|
stats.gen_message_random++;
|
|
@@ -568,6 +585,7 @@ const genShake = () => (opts) => (pubSeed, skSeed) => {
|
|
|
568
585
|
clean: () => {
|
|
569
586
|
h0.destroy();
|
|
570
587
|
h0tmp.destroy();
|
|
588
|
+
cleanBytes(thashOut, prfOut);
|
|
571
589
|
//console.log(stats);
|
|
572
590
|
},
|
|
573
591
|
};
|
|
@@ -635,6 +653,16 @@ const genSha = (h0, h1) => (opts) => (pub_seed, sk_seed) => {
|
|
|
635
653
|
.update(new Uint8Array(h1.blockLen - N));
|
|
636
654
|
const h0tmp = h0ps.clone();
|
|
637
655
|
const h1tmp = h1ps.clone();
|
|
656
|
+
// Per-context output scratch: thash1/thashN/PRFaddr return views into these buffers, so
|
|
657
|
+
// callers must consume or copy a result before the next call on the same lane (see Context
|
|
658
|
+
// docs). digestInto also skips digest()'s per-call destroy(): the tmp states are fully
|
|
659
|
+
// overwritten by the next _cloneInto and wiped in clean().
|
|
660
|
+
const h0out = new Uint8Array(h0.outputLen);
|
|
661
|
+
const h1out = new Uint8Array(h1.outputLen);
|
|
662
|
+
const prfOut = new Uint8Array(h0.outputLen);
|
|
663
|
+
const h0outN = h0out.subarray(0, N);
|
|
664
|
+
const h1outN = h1out.subarray(0, N);
|
|
665
|
+
const prfOutN = prfOut.subarray(0, N);
|
|
638
666
|
// https://www.rfc-editor.org/rfc/rfc8017.html#appendix-B.2.1
|
|
639
667
|
// This local helper is intentionally stricter than generic MGF1 reuse: current SLH-DSA callers
|
|
640
668
|
// only request tiny `m`-byte outputs, but the guard below rejects `length > 2^32` instead of
|
|
@@ -653,27 +681,26 @@ const genSha = (h0, h1) => (opts) => (pub_seed, sk_seed) => {
|
|
|
653
681
|
cleanBytes(out.subarray(length));
|
|
654
682
|
return out.subarray(0, length);
|
|
655
683
|
}
|
|
656
|
-
const thash = (
|
|
684
|
+
const thash = (h, hTmp, out, outN) => (blocks, input, addr) => {
|
|
657
685
|
stats.thash++;
|
|
658
|
-
const
|
|
659
|
-
|
|
686
|
+
const len = blocks * N;
|
|
687
|
+
h._cloneInto(hTmp)
|
|
660
688
|
.update(addr)
|
|
661
|
-
.update(input.subarray(0,
|
|
662
|
-
.
|
|
663
|
-
return
|
|
689
|
+
.update(input.length === len ? input : input.subarray(0, len))
|
|
690
|
+
.digestInto(out);
|
|
691
|
+
return outN;
|
|
664
692
|
};
|
|
665
693
|
return {
|
|
666
694
|
PRFaddr: (addr) => {
|
|
667
695
|
if (!sk_seed)
|
|
668
696
|
throw new Error('No sk seed');
|
|
669
697
|
stats.prf++;
|
|
670
|
-
|
|
698
|
+
h0ps
|
|
671
699
|
._cloneInto(h0tmp)
|
|
672
700
|
.update(addr)
|
|
673
701
|
.update(sk_seed)
|
|
674
|
-
.
|
|
675
|
-
|
|
676
|
-
return res;
|
|
702
|
+
.digestInto(prfOut);
|
|
703
|
+
return prfOutN;
|
|
677
704
|
},
|
|
678
705
|
PRFmsg: (skPRF, random, msg) => {
|
|
679
706
|
stats.gen_message_random++;
|
|
@@ -689,13 +716,14 @@ const genSha = (h0, h1) => (opts) => (pub_seed, sk_seed) => {
|
|
|
689
716
|
const seed = concatBytes(R.subarray(0, N), pk.subarray(0, N), h1.create().update(R.subarray(0, N)).update(pk).update(m).digest());
|
|
690
717
|
return mgf1(seed, outLen, h1);
|
|
691
718
|
},
|
|
692
|
-
thash1: thash(
|
|
693
|
-
thashN: thash(
|
|
719
|
+
thash1: thash(h0ps, h0tmp, h0out, h0outN).bind(null, 1),
|
|
720
|
+
thashN: thash(h1ps, h1tmp, h1out, h1outN),
|
|
694
721
|
clean: () => {
|
|
695
722
|
h0ps.destroy();
|
|
696
723
|
h1ps.destroy();
|
|
697
724
|
h0tmp.destroy();
|
|
698
725
|
h1tmp.destroy();
|
|
726
|
+
cleanBytes(h0out, h1out, prfOut);
|
|
699
727
|
//console.log(stats);
|
|
700
728
|
},
|
|
701
729
|
};
|
|
@@ -712,6 +740,23 @@ const SHA512_SIMPLE = /* @__PURE__ */ (() => ({
|
|
|
712
740
|
* SLH-DSA-SHA2-128f: Table 2 row `n=16, h=66, d=22, h'=3, a=6, k=33, lg w=4, m=34`;
|
|
713
741
|
* lengths `publicKey=32`, `secretKey=64`, `signature=17088`, `seed=48`, `signRand=16`.
|
|
714
742
|
* Also exposes `.prehash(...)`.
|
|
743
|
+
* @example
|
|
744
|
+
* Generate deterministic SLH-DSA keys, sign one message, and verify the signature.
|
|
745
|
+
* ```ts
|
|
746
|
+
* import { sha256 } from '@noble/hashes/sha2.js';
|
|
747
|
+
* import { slh_dsa_sha2_128f } from '@noble/post-quantum/slh-dsa.js';
|
|
748
|
+
* const seed = new Uint8Array(slh_dsa_sha2_128f.lengths.seed!);
|
|
749
|
+
* const { secretKey, publicKey } = slh_dsa_sha2_128f.keygen(seed);
|
|
750
|
+
* const msg = new TextEncoder().encode('hello noble');
|
|
751
|
+
* const sig = slh_dsa_sha2_128f.sign(msg, secretKey);
|
|
752
|
+
* const isValid = slh_dsa_sha2_128f.verify(sig, msg, publicKey);
|
|
753
|
+
* const recovered = slh_dsa_sha2_128f.getPublicKey(secretKey);
|
|
754
|
+
* const context = new Uint8Array([1, 2, 3]);
|
|
755
|
+
* const prehash = slh_dsa_sha2_128f.prehash(sha256);
|
|
756
|
+
* const preSig = prehash.sign(msg, secretKey, { context });
|
|
757
|
+
* const preValid = prehash.verify(preSig, msg, publicKey, { context });
|
|
758
|
+
* const internalSig = slh_dsa_sha2_128f.internal.sign(msg, secretKey);
|
|
759
|
+
* ```
|
|
715
760
|
*/
|
|
716
761
|
export const slh_dsa_sha2_128f = /* @__PURE__ */ (() => gen(PARAMS['128f'], SHA256_SIMPLE))();
|
|
717
762
|
/**
|
|
@@ -744,4 +789,3 @@ export const slh_dsa_sha2_256f = /* @__PURE__ */ (() => gen(PARAMS['256f'], SHA5
|
|
|
744
789
|
* Also exposes `.prehash(...)`.
|
|
745
790
|
*/
|
|
746
791
|
export const slh_dsa_sha2_256s = /* @__PURE__ */ (() => gen(PARAMS['256s'], SHA512_SIMPLE))();
|
|
747
|
-
//# sourceMappingURL=slh-dsa.js.map
|
package/src/_crystals.ts
CHANGED
|
@@ -73,9 +73,15 @@ type Crystals<T extends TypedArray> = {
|
|
|
73
73
|
smod: (a: number, modulo?: number) => number;
|
|
74
74
|
nttZetas: T;
|
|
75
75
|
NTT: {
|
|
76
|
-
/**
|
|
76
|
+
/**
|
|
77
|
+
* Forward transform in place. Mutates and returns `r`.
|
|
78
|
+
* Kyber-mode input coefficients must already use canonical representatives in `[0, Q)`.
|
|
79
|
+
*/
|
|
77
80
|
encode: (r: T) => T;
|
|
78
|
-
/**
|
|
81
|
+
/**
|
|
82
|
+
* Inverse transform in place. Mutates and returns `r`.
|
|
83
|
+
* Kyber-mode input coefficients must already use canonical representatives in `[0, Q)`.
|
|
84
|
+
*/
|
|
79
85
|
decode: (r: T) => T;
|
|
80
86
|
};
|
|
81
87
|
bitsCoder: (d: number, c: Coder<number, number>) => BytesCoderLen<T>;
|
|
@@ -135,14 +141,34 @@ export const genCrystals = <T extends TypedArray>(opts: CrystalOpts<T>): TRet<Cr
|
|
|
135
141
|
// Kyber has slightly different params, since there is no 512th primitive root of unity mod q,
|
|
136
142
|
// only 256th primitive root of unity mod. Which also complicates MultiplyNTT.
|
|
137
143
|
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
sub: (a: number, b: number) => mod((a | 0) - (b | 0)) | 0,
|
|
141
|
-
mul: (a: number, b: number) => mod((a | 0) * (b | 0)) | 0,
|
|
142
|
-
inv: (_a: number) => {
|
|
143
|
-
throw new Error('not implemented');
|
|
144
|
-
},
|
|
144
|
+
const inv = (_a: number) => {
|
|
145
|
+
throw new Error('not implemented');
|
|
145
146
|
};
|
|
147
|
+
// ML-KEM (Kyber) polynomials always enter the transform reduced to [0, Q), so add/sub only
|
|
148
|
+
// need one conditional correction instead of `%`; measured ~20% faster NTT there.
|
|
149
|
+
// ML-DSA keeps the generic mod() path on purpose: its first forward stage sees centered
|
|
150
|
+
// (negative) coefficients, and `sub(a, t)` can drop below -Q (t is a mul output in [0, Q)),
|
|
151
|
+
// so a single correction is not enough. A guarded fast path with mod() fallback was measured
|
|
152
|
+
// slower than plain `%` for the 23-bit Q (V8 int32 modulo is one div; the branches lose).
|
|
153
|
+
const field = isKyber
|
|
154
|
+
? {
|
|
155
|
+
add: (a: number, b: number) => {
|
|
156
|
+
const r = (a + b) | 0;
|
|
157
|
+
return r >= Q ? (r - Q) | 0 : r;
|
|
158
|
+
},
|
|
159
|
+
sub: (a: number, b: number) => {
|
|
160
|
+
const r = (a - b) | 0;
|
|
161
|
+
return r < 0 ? (r + Q) | 0 : r;
|
|
162
|
+
},
|
|
163
|
+
mul: (a: number, b: number) => mod((a | 0) * (b | 0)) | 0,
|
|
164
|
+
inv,
|
|
165
|
+
}
|
|
166
|
+
: {
|
|
167
|
+
add: (a: number, b: number) => mod((a | 0) + (b | 0)) | 0,
|
|
168
|
+
sub: (a: number, b: number) => mod((a | 0) - (b | 0)) | 0,
|
|
169
|
+
mul: (a: number, b: number) => mod((a | 0) * (b | 0)) | 0,
|
|
170
|
+
inv,
|
|
171
|
+
};
|
|
146
172
|
const nttOpts = {
|
|
147
173
|
N,
|
|
148
174
|
roots: nttZetas as any,
|
|
@@ -168,6 +194,12 @@ export const genCrystals = <T extends TypedArray>(opts: CrystalOpts<T>): TRet<Cr
|
|
|
168
194
|
// Pack one little-endian `d`-bit word per coefficient, matching FIPS 203 ByteEncode /
|
|
169
195
|
// ByteDecode and the FIPS 204 BitsToBytes-based polynomial packing helpers.
|
|
170
196
|
const bitsCoder = (d: number, c: Coder<number, number>): TRet<BytesCoderLen<T>> => {
|
|
197
|
+
// Validate the carry shape once: JS bitwise operations silently truncate wider accumulators.
|
|
198
|
+
for (let i = 0, bufLen = 0; i < N; i++) {
|
|
199
|
+
bufLen += d;
|
|
200
|
+
if (bufLen > 32) getMask(bufLen);
|
|
201
|
+
bufLen %= 8;
|
|
202
|
+
}
|
|
171
203
|
const mask = getMask(d);
|
|
172
204
|
const bytesLen = d * (N / 8);
|
|
173
205
|
return {
|
|
@@ -178,7 +210,9 @@ export const genCrystals = <T extends TypedArray>(opts: CrystalOpts<T>): TRet<Cr
|
|
|
178
210
|
for (let i = 0, buf = 0, bufLen = 0, pos = 0; i < poly.length; i++) {
|
|
179
211
|
buf |= (c.encode(poly[i]) & mask) << bufLen;
|
|
180
212
|
bufLen += d;
|
|
181
|
-
|
|
213
|
+
// Take the low byte directly: `& 0xff` matches the previous getMask(bufLen) result
|
|
214
|
+
// after Uint8Array truncation, without a validated function call per output byte.
|
|
215
|
+
for (; bufLen >= 8; bufLen -= 8, buf >>= 8) r[pos++] = buf & 0xff;
|
|
182
216
|
}
|
|
183
217
|
return r as TRet<Uint8Array>;
|
|
184
218
|
},
|
package/src/falcon.ts
CHANGED
|
@@ -443,6 +443,17 @@ const Q: number = 12289; // 12 * 1024 + 1
|
|
|
443
443
|
// Falcon's midpoint floor(q/2); the only live use is the mirrored G-reconstruction reduction below.
|
|
444
444
|
const Qhalf: number = Q >> 1;
|
|
445
445
|
const QBig = BigInt(Q);
|
|
446
|
+
const _0n = /* @__PURE__ */ BigInt(0);
|
|
447
|
+
const _1n = /* @__PURE__ */ BigInt(1);
|
|
448
|
+
const _10n = /* @__PURE__ */ BigInt(10);
|
|
449
|
+
const _25n = /* @__PURE__ */ BigInt(25);
|
|
450
|
+
const _31n = /* @__PURE__ */ BigInt(31);
|
|
451
|
+
const _32n = /* @__PURE__ */ BigInt(32);
|
|
452
|
+
const _63n = /* @__PURE__ */ BigInt(63);
|
|
453
|
+
const _64n = /* @__PURE__ */ BigInt(64);
|
|
454
|
+
// Low 32 bits and low 63 bits of a bigint, used by the chacha20 counter and gaussian sampler.
|
|
455
|
+
const MASK_32n = /* @__PURE__ */ BigInt('0xffffffff');
|
|
456
|
+
const MASK_63n = /* @__PURE__ */ BigInt('0x7fffffffffffffff');
|
|
446
457
|
//const R = 4091; // 2^16 mod q
|
|
447
458
|
// This 16-bit Montgomery kernel uses R = 2^16, so mul(x, R2) converts x into Montgomery form.
|
|
448
459
|
const R2 = 10952; // 2^32 mod q
|
|
@@ -479,35 +490,35 @@ const BITLENGTH = [
|
|
|
479
490
|
// Smaller Falcon dimensions reuse the N = 1024, q = 12289 table by summing 2^(10-logn) draws.
|
|
480
491
|
// The trailing 0 sentinel guarantees gaussSingle()
|
|
481
492
|
// always selects a tail bucket when x = 0 is missed.
|
|
482
|
-
const gauss_1024_12289 = [
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
];
|
|
493
|
+
const gauss_1024_12289 = /* @__PURE__ */ [
|
|
494
|
+
'1283868770400643928',
|
|
495
|
+
'6416574995475331444',
|
|
496
|
+
'4078260278032692663',
|
|
497
|
+
'2353523259288686585',
|
|
498
|
+
'1227179971273316331',
|
|
499
|
+
'575931623374121527',
|
|
500
|
+
'242543240509105209',
|
|
501
|
+
'91437049221049666',
|
|
502
|
+
'30799446349977173',
|
|
503
|
+
'9255276791179340',
|
|
504
|
+
'2478152334826140',
|
|
505
|
+
'590642893610164',
|
|
506
|
+
'125206034929641',
|
|
507
|
+
'23590435911403',
|
|
508
|
+
'3948334035941',
|
|
509
|
+
'586753615614',
|
|
510
|
+
'77391054539',
|
|
511
|
+
'9056793210',
|
|
512
|
+
'940121950',
|
|
513
|
+
'86539696',
|
|
514
|
+
'7062824',
|
|
515
|
+
'510971',
|
|
516
|
+
'32764',
|
|
517
|
+
'1862',
|
|
518
|
+
'94',
|
|
519
|
+
'4',
|
|
520
|
+
'0',
|
|
521
|
+
].map(BigInt);
|
|
511
522
|
|
|
512
523
|
// Exact binary64 1/sigma payloads from round-3 fpr.h. Nearby decimal spellings round 1 ULP low in
|
|
513
524
|
// JS, so keep these as decoded bit patterns and recheck the raw payloads after edits.
|
|
@@ -1248,9 +1259,9 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
|
|
|
1248
1259
|
let val = 0;
|
|
1249
1260
|
for (let i = 0; i < g; i++) {
|
|
1250
1261
|
const r128 = bytesToNumberLE(this.shake.xof(16));
|
|
1251
|
-
const r1 = r128 &
|
|
1252
|
-
const r2 = (r128 >>
|
|
1253
|
-
const sign = Number((r128 >>
|
|
1262
|
+
const r1 = r128 & MASK_63n;
|
|
1263
|
+
const r2 = (r128 >> _64n) & MASK_63n;
|
|
1264
|
+
const sign = Number((r128 >> _63n) & _1n);
|
|
1254
1265
|
let f = r1 < gauss_1024_12289[0] ? 1 : 0;
|
|
1255
1266
|
let v = 0;
|
|
1256
1267
|
for (let k = 1; k < gauss_1024_12289.length; k++) {
|
|
@@ -1283,13 +1294,13 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
|
|
|
1283
1294
|
const n = 1 << logn;
|
|
1284
1295
|
const d = new Array(n >> 1);
|
|
1285
1296
|
for (let k = 0; k < n; k += 2) {
|
|
1286
|
-
let s: bigint =
|
|
1297
|
+
let s: bigint = _0n;
|
|
1287
1298
|
for (let i = 0; i <= k; i += 2) s += a[i] * a[k - i];
|
|
1288
1299
|
for (let i = k + 2; i < n; i += 2) s -= a[i] * a[k + n - i];
|
|
1289
1300
|
d[k >>> 1] = s;
|
|
1290
1301
|
}
|
|
1291
1302
|
for (let k = 0; k < n; k += 2) {
|
|
1292
|
-
let s: bigint =
|
|
1303
|
+
let s: bigint = _0n;
|
|
1293
1304
|
for (let i = 1; i < k; i += 2) s += a[i] * a[k - i];
|
|
1294
1305
|
for (let i = k + 1; i < n; i += 2) s -= a[i] * a[k + n - i];
|
|
1295
1306
|
d[k >>> 1] -= s;
|
|
@@ -1299,7 +1310,7 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
|
|
|
1299
1310
|
private mulConjD(logn: number, d: BPoly, a: BPoly, b: BPoly): BPoly {
|
|
1300
1311
|
const n = 1 << logn;
|
|
1301
1312
|
for (let k = 0; k < n; k++) {
|
|
1302
|
-
let s: bigint =
|
|
1313
|
+
let s: bigint = _0n;
|
|
1303
1314
|
for (let i = 0; i <= k; i += 2) s += b[i >>> 1] * a[k - i];
|
|
1304
1315
|
for (let i = k + 2 - (k & 1); i < n; i += 2) s -= b[i >>> 1] * a[k + n - i];
|
|
1305
1316
|
if ((k & 1) === 0) d[k] = s;
|
|
@@ -1310,7 +1321,7 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
|
|
|
1310
1321
|
private subMul(logn: number, a: BPoly, b: BPoly, c: BPoly, e: bigint): BPoly {
|
|
1311
1322
|
const n = 1 << logn;
|
|
1312
1323
|
for (let k = 0; k < n; k++) {
|
|
1313
|
-
let s: bigint =
|
|
1324
|
+
let s: bigint = _0n;
|
|
1314
1325
|
for (let i = 0; i <= k; i++) s += b[i] * c[k - i];
|
|
1315
1326
|
for (let i = k + 1; i < n; i++) s -= b[i] * c[k + n - i];
|
|
1316
1327
|
a[k] -= s << e;
|
|
@@ -1353,7 +1364,7 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
|
|
|
1353
1364
|
const Gx = new Float64Array(n);
|
|
1354
1365
|
const k = new Array(n);
|
|
1355
1366
|
while (true) {
|
|
1356
|
-
let scaleFG =
|
|
1367
|
+
let scaleFG = _31n * (FGlen - _10n);
|
|
1357
1368
|
for (let i = 0; i < n; i++) {
|
|
1358
1369
|
Fx[i] = Number(F[i] >> scaleFG);
|
|
1359
1370
|
Gx[i] = Number(G[i] >> scaleFG);
|
|
@@ -1373,12 +1384,12 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
|
|
|
1373
1384
|
}
|
|
1374
1385
|
F = this.subMul(logn, F, f, k, scaleK); // 3: F ← F - kf
|
|
1375
1386
|
G = this.subMul(logn, G, g, k, scaleK); // 4: G ← G - kg
|
|
1376
|
-
const maxfgNew = scaleK + BigInt(Math.round(fgMaxBits)) +
|
|
1387
|
+
const maxfgNew = scaleK + BigInt(Math.round(fgMaxBits)) + _10n;
|
|
1377
1388
|
if (maxfgNew < maxFGBits) maxFGBits = maxfgNew;
|
|
1378
|
-
if (FGlen >
|
|
1379
|
-
if (scaleK <=
|
|
1380
|
-
scaleK -=
|
|
1381
|
-
if (scaleK <
|
|
1389
|
+
if (FGlen > _1n && FGlen * _31n >= maxFGBits + _31n) FGlen--;
|
|
1390
|
+
if (scaleK <= _0n) break;
|
|
1391
|
+
scaleK -= _25n;
|
|
1392
|
+
if (scaleK < _0n) scaleK = _0n;
|
|
1382
1393
|
}
|
|
1383
1394
|
return true;
|
|
1384
1395
|
}
|
|
@@ -1406,10 +1417,10 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
|
|
|
1406
1417
|
const xf = f[0];
|
|
1407
1418
|
const xg = g[0];
|
|
1408
1419
|
// We can rely on 'invert' to throw if they are not coprime.
|
|
1409
|
-
if (xf <=
|
|
1420
|
+
if (xf <= _0n || xg <= _0n) return false;
|
|
1410
1421
|
try {
|
|
1411
1422
|
const u1 = invert(xf, xg); // if gcd(f, g) ≠ 1 then
|
|
1412
|
-
const v1 = (
|
|
1423
|
+
const v1 = (_1n - u1 * xf) / xg;
|
|
1413
1424
|
F[0] = -v1 * QBig; // 5: (F, G) ← (vq, uq)
|
|
1414
1425
|
G[0] = u1 * QBig;
|
|
1415
1426
|
return true;
|
|
@@ -1672,7 +1683,13 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
|
|
|
1672
1683
|
).encode([nonce, pad(sigLen).encode(s2), msg]);
|
|
1673
1684
|
},
|
|
1674
1685
|
decode(data: TArg<Uint8Array>): TRet<SignatureRaw> {
|
|
1686
|
+
// Keep API misuse on the coder's TypeError path before reading the container length.
|
|
1687
|
+
abytes(data, undefined, 'signature');
|
|
1688
|
+
// The compressed-signature field is fixed-width here; only the message is variable. A
|
|
1689
|
+
// container shorter than the fixed part would make the s2 field borrow bytes from nowhere
|
|
1690
|
+
// and let a truncated encoding open to the same message.
|
|
1675
1691
|
const msgLen = data.length - NONCELEN - sigLen - 1;
|
|
1692
|
+
if (msgLen < 0) throw new Error('signature coder: wrong length');
|
|
1676
1693
|
const [nonce, s2, msg] = headerCoder(
|
|
1677
1694
|
0x30 + logn,
|
|
1678
1695
|
splitCoder('falcon.signature', NONCELEN, sigLen, msgLen)
|
|
@@ -1696,9 +1713,15 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
|
|
|
1696
1713
|
nonce: Uint8Array;
|
|
1697
1714
|
s2: Uint8Array;
|
|
1698
1715
|
}> {
|
|
1716
|
+
// Padded detached signatures are fixed-length (`lengths.signature`), so the payload width
|
|
1717
|
+
// must come from the parameter set, not from the input: deriving it would accept appended
|
|
1718
|
+
// zero bytes and truncated padding as extra valid encodings of the same signature.
|
|
1719
|
+
// Unpadded signatures are variable-length; decodeUnpaddedSig() enforces the exact canonical
|
|
1720
|
+
// bitlength of whatever remains.
|
|
1721
|
+
const payloadLen = opts.padded ? sigLen : data.length - NONCELEN - 1;
|
|
1699
1722
|
const [nonce, raw] = headerCoder(
|
|
1700
1723
|
0x30 + logn,
|
|
1701
|
-
splitCoder('falcon.detachedSignature', NONCELEN,
|
|
1724
|
+
splitCoder('falcon.detachedSignature', NONCELEN, payloadLen)
|
|
1702
1725
|
).decode(data);
|
|
1703
1726
|
const s2 = decodeSig(raw);
|
|
1704
1727
|
return { nonce, s2 } as TRet<{ nonce: Uint8Array; s2: Uint8Array }>;
|
|
@@ -1785,7 +1808,7 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
|
|
|
1785
1808
|
private shakeBuf: Uint8Array;
|
|
1786
1809
|
private ctrView: DataView;
|
|
1787
1810
|
// ChaCha
|
|
1788
|
-
private ctr: bigint =
|
|
1811
|
+
private ctr: bigint = _0n;
|
|
1789
1812
|
private buf: Uint8Array;
|
|
1790
1813
|
private buf32: Uint32Array;
|
|
1791
1814
|
private pos: number;
|
|
@@ -1835,8 +1858,8 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
|
|
|
1835
1858
|
const out32 = swap32IfBE(this.buf32);
|
|
1836
1859
|
for (let i = 0; i < 8; i++, this.ctr++) {
|
|
1837
1860
|
const n = swap32IfBE(this.nonce32.slice()); // [n0, n1, n2, n3]
|
|
1838
|
-
n[2] ^= Number(this.ctr &
|
|
1839
|
-
n[3] ^= Number(this.ctr >>
|
|
1861
|
+
n[2] ^= Number(this.ctr & MASK_32n);
|
|
1862
|
+
n[3] ^= Number(this.ctr >> _32n);
|
|
1840
1863
|
// chacha20() takes raw nonce bytes; on BE the word-normalized temp must be swapped back.
|
|
1841
1864
|
swap32IfBE(n.subarray(1));
|
|
1842
1865
|
chacha20(this.key, u8(n.subarray(1)), EMPTY_CHACHA20_BLOCK, this.curBlock, n[0]);
|
|
@@ -2181,7 +2204,8 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
|
|
|
2181
2204
|
// ▷ Remove 1 byte for the header, and 40 bytes for r
|
|
2182
2205
|
// 11: while (s = ⊥)
|
|
2183
2206
|
// 12: return sig = (r, s)
|
|
2184
|
-
abytes(msg);
|
|
2207
|
+
abytes(msg, undefined, 'msg');
|
|
2208
|
+
abytes(sk, secretKeyCoder.bytesLen, 'secretKey');
|
|
2185
2209
|
// One RNG stream drives both the public 40-byte nonce and the 48-byte sampler seed, so
|
|
2186
2210
|
// deterministic rnd hooks make signatures deterministic for fixed secretKey/message inputs.
|
|
2187
2211
|
const nonce = rnd(40);
|
|
@@ -2289,6 +2313,8 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
|
|
|
2289
2313
|
const getRnd = (opts: TArg<FalconSigOpts> = {}): TRet<FalconRandom> => {
|
|
2290
2314
|
validateSigOpts(opts);
|
|
2291
2315
|
if (opts.context !== undefined) throw new Error('context is not supported');
|
|
2316
|
+
if (opts.random !== undefined && typeof opts.random !== 'function')
|
|
2317
|
+
throw new TypeError('"opts.random" expected function, got type=' + typeof opts.random);
|
|
2292
2318
|
if (opts.random !== undefined) return opts.random as TRet<FalconRandom>;
|
|
2293
2319
|
if (opts.extraEntropy === undefined) return randomBytes;
|
|
2294
2320
|
const seed = opts.extraEntropy === false ? new Uint8Array(48) : opts.extraEntropy;
|
|
@@ -2328,6 +2354,7 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
|
|
|
2328
2354
|
}>;
|
|
2329
2355
|
};
|
|
2330
2356
|
const getPublicKey = (sk: TArg<Uint8Array>): TRet<Uint8Array> => {
|
|
2357
|
+
abytes(sk, secretKeyCoder.bytesLen, 'secretKey');
|
|
2331
2358
|
const [f, g, F] = secretKeyCoder.decode(sk);
|
|
2332
2359
|
try {
|
|
2333
2360
|
const h = computePublic(f, g);
|
|
@@ -2358,9 +2385,10 @@ function genFalcon(opts: FalconOpts): TRet<Falcon> {
|
|
|
2358
2385
|
verOpts: TArg<VerOpts> = {}
|
|
2359
2386
|
) => {
|
|
2360
2387
|
checkVerOpts(verOpts);
|
|
2361
|
-
abytes(sig);
|
|
2362
|
-
abytes(msg);
|
|
2363
|
-
|
|
2388
|
+
abytes(sig, undefined, 'signature');
|
|
2389
|
+
abytes(msg, undefined, 'msg');
|
|
2390
|
+
// Length/canonical public-key failures are decoded below and return false; only type is fatal.
|
|
2391
|
+
abytes(pk, undefined, 'publicKey');
|
|
2364
2392
|
try {
|
|
2365
2393
|
const { s2, nonce } = SignatureCoderDetached(logn).decode(sig);
|
|
2366
2394
|
return verifyRaw(pk, s2, nonce, msg);
|