@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/slh-dsa.js
CHANGED
|
@@ -28,10 +28,17 @@
|
|
|
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";
|
|
36
|
+
// Keys the internal SLH-DSA surface accepts. `context` is deliberately absent: the public
|
|
37
|
+
// wrappers consume it when they format M' and must not forward it, because a key that is
|
|
38
|
+
// accepted and then never read is the same silent downgrade this validation exists to prevent.
|
|
39
|
+
// `extraEntropy` is signing-only, so verification (which takes no options of its own) has none.
|
|
40
|
+
const INTERNAL_SIG_OPT_KEYS = /* @__PURE__ */ Object.freeze(['extraEntropy']);
|
|
41
|
+
const INTERNAL_VER_OPT_KEYS = /* @__PURE__ */ Object.freeze([]);
|
|
35
42
|
/** Winternitz signature params. */
|
|
36
43
|
/**
|
|
37
44
|
* Built-in SLH-DSA Table 2 subset keyed by strength/profile.
|
|
@@ -61,19 +68,6 @@ const AddressType = {
|
|
|
61
68
|
WOTSPRF: 5,
|
|
62
69
|
FORSPRF: 6,
|
|
63
70
|
};
|
|
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
71
|
// Local FIPS 205 Algorithm 4 `base_2^b(...)` implementation. Bits are consumed in big-endian
|
|
78
72
|
// order within each input byte, and callers must provide at least `ceil(outLen * b / 8)` bytes;
|
|
79
73
|
// short inputs are not rejected and would zero-extend implicitly.
|
|
@@ -92,8 +86,11 @@ const base2b = (outLen, b) => {
|
|
|
92
86
|
return baseB;
|
|
93
87
|
};
|
|
94
88
|
};
|
|
89
|
+
const _1n = /* @__PURE__ */ BigInt(1);
|
|
90
|
+
const _8n = /* @__PURE__ */ BigInt(8);
|
|
91
|
+
const _0xffn = /* @__PURE__ */ BigInt(0xff);
|
|
95
92
|
function getMaskBig(bits) {
|
|
96
|
-
return (
|
|
93
|
+
return (_1n << BigInt(bits)) - _1n; // 4 -> 0b1111
|
|
97
94
|
}
|
|
98
95
|
/** One parameter/hash instantiation of the public SLH-DSA API.
|
|
99
96
|
* `keygen(seed)` is a deterministic 3N-byte library hook around the internal keygen flow,
|
|
@@ -137,9 +134,18 @@ function gen(opts, hashOpts_) {
|
|
|
137
134
|
// `height` / `chain` and `index` / `hash` share the same spec words, so callers must use the
|
|
138
135
|
// address-type-specific combinations instead of mixing both meanings in one call.
|
|
139
136
|
const setAddr = (opts, addr = new Uint8Array(ADDR_BYTES)) => {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const
|
|
137
|
+
// These objects are created in hot internal loops, so avoid cloning them. Read only own fields:
|
|
138
|
+
// absent address words must stay absent even if Object.prototype was polluted.
|
|
139
|
+
const type = Object.hasOwn(opts, 'type') ? opts.type : undefined;
|
|
140
|
+
const height = Object.hasOwn(opts, 'height') ? opts.height : undefined;
|
|
141
|
+
const tree = Object.hasOwn(opts, 'tree') ? opts.tree : undefined;
|
|
142
|
+
const layer = Object.hasOwn(opts, 'layer') ? opts.layer : undefined;
|
|
143
|
+
const index = Object.hasOwn(opts, 'index') ? opts.index : undefined;
|
|
144
|
+
const chain = Object.hasOwn(opts, 'chain') ? opts.chain : undefined;
|
|
145
|
+
const hash = Object.hasOwn(opts, 'hash') ? opts.hash : undefined;
|
|
146
|
+
const keypair = Object.hasOwn(opts, 'keypair') ? opts.keypair : undefined;
|
|
147
|
+
const subtreeAddr = Object.hasOwn(opts, 'subtreeAddr') ? opts.subtreeAddr : undefined;
|
|
148
|
+
const keypairAddr = Object.hasOwn(opts, 'keypairAddr') ? opts.keypairAddr : undefined;
|
|
143
149
|
if (height !== undefined)
|
|
144
150
|
addr[OFFSET_CHAIN_ADDR] = height;
|
|
145
151
|
if (layer !== undefined)
|
|
@@ -150,12 +156,21 @@ function gen(opts, hashOpts_) {
|
|
|
150
156
|
addr[OFFSET_CHAIN_ADDR] = chain;
|
|
151
157
|
if (hash !== undefined)
|
|
152
158
|
addr[OFFSET_HASH_ADDR] = hash;
|
|
153
|
-
|
|
154
|
-
|
|
159
|
+
// Manual big-endian writes: setAddr runs in the innermost WOTS/tree loops, and creating a
|
|
160
|
+
// DataView per call was a measurable share of sign() time.
|
|
161
|
+
if (index !== undefined) {
|
|
162
|
+
addr[OFFSET_TREE_INDEX + 0] = index >>> 24;
|
|
163
|
+
addr[OFFSET_TREE_INDEX + 1] = index >>> 16;
|
|
164
|
+
addr[OFFSET_TREE_INDEX + 2] = index >>> 8;
|
|
165
|
+
addr[OFFSET_TREE_INDEX + 3] = index;
|
|
166
|
+
}
|
|
155
167
|
if (subtreeAddr)
|
|
156
168
|
addr.set(subtreeAddr.subarray(0, OFFSET_TREE + 8));
|
|
157
|
-
if (tree !== undefined)
|
|
158
|
-
|
|
169
|
+
if (tree !== undefined) {
|
|
170
|
+
let t = tree;
|
|
171
|
+
for (let i = 7; i >= 0; i--, t >>= _8n)
|
|
172
|
+
addr[OFFSET_TREE + i] = Number(t & _0xffn);
|
|
173
|
+
}
|
|
159
174
|
if (keypair !== undefined) {
|
|
160
175
|
addr[OFFSET_KP_ADDR1] = keypair;
|
|
161
176
|
if (TREE_HEIGHT > 8)
|
|
@@ -209,10 +224,12 @@ function gen(opts, hashOpts_) {
|
|
|
209
224
|
const maxIdx = (1 << height) - 1;
|
|
210
225
|
const stack = new Uint8Array(height * N);
|
|
211
226
|
const authPath = new Uint8Array(height * N);
|
|
227
|
+
// One node buffer per treehash call (not per leaf): both halves are fully overwritten at
|
|
228
|
+
// each use, and the returned root aliases cur1, which is never reused after return.
|
|
229
|
+
const current = new Uint8Array(2 * N);
|
|
230
|
+
const cur0 = current.subarray(0, N);
|
|
231
|
+
const cur1 = current.subarray(N);
|
|
212
232
|
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
233
|
const addrOffset = idx + idxOffset;
|
|
217
234
|
cur1.set(leafFn(leafIdx, addrOffset, rawContext, info));
|
|
218
235
|
let h = 0;
|
|
@@ -360,7 +377,7 @@ function gen(opts, hashOpts_) {
|
|
|
360
377
|
return Uint8Array.from(pk);
|
|
361
378
|
},
|
|
362
379
|
sign: (msg, sk, opts = {}) => {
|
|
363
|
-
validateSigOpts(opts);
|
|
380
|
+
opts = validateSigOpts(opts, INTERNAL_SIG_OPT_KEYS);
|
|
364
381
|
let { extraEntropy: random } = opts;
|
|
365
382
|
const [skSeed, skPRF, pk] = secretCoder.decode(sk); // todo: fix
|
|
366
383
|
const [pkSeed, _] = publicCoder.decode(pk);
|
|
@@ -394,7 +411,9 @@ function gen(opts, hashOpts_) {
|
|
|
394
411
|
height: 0,
|
|
395
412
|
index: indices[i] + idxOffset,
|
|
396
413
|
}, forsTreeAddr);
|
|
397
|
-
|
|
414
|
+
// Copy: PRFaddr returns a per-context scratch view, and this value is retained in
|
|
415
|
+
// `fors` across the many PRFaddr calls inside forsTreehash below.
|
|
416
|
+
const prf = copyBytes(context.PRFaddr(forsTreeAddr));
|
|
398
417
|
setAddr({ type: AddressType.FORSTREE }, forsTreeAddr);
|
|
399
418
|
const { root, authPath } = forsTreehash(context, indices[i], idxOffset, forsTreeAddr, forsLeaf);
|
|
400
419
|
roots.push(root);
|
|
@@ -404,7 +423,9 @@ function gen(opts, hashOpts_) {
|
|
|
404
423
|
type: AddressType.FORSPK,
|
|
405
424
|
keypairAddr: wotsAddr,
|
|
406
425
|
});
|
|
407
|
-
|
|
426
|
+
// Copy: thashN returns a per-context scratch view, and `root` lives across every hash
|
|
427
|
+
// call in the hypertree loop below (it is also mutated via root.set).
|
|
428
|
+
const root = copyBytes(context.thashN(K, concatBytes(...roots), forsPkAddr));
|
|
408
429
|
// WOTS signatures
|
|
409
430
|
const treeAddr = setAddr({ type: AddressType.HASHTREE });
|
|
410
431
|
const wots = [];
|
|
@@ -422,12 +443,20 @@ function gen(opts, hashOpts_) {
|
|
|
422
443
|
cleanBytes(R, random, treeAddr, wotsAddr, forsLeaf, forsTreeAddr, indices, roots);
|
|
423
444
|
return SIG;
|
|
424
445
|
},
|
|
425
|
-
verify: (sig, msg, publicKey) => {
|
|
446
|
+
verify: (sig, msg, publicKey, opts = {}) => {
|
|
447
|
+
// The internal verify reads no options; reject any so a stray key (e.g. a caller
|
|
448
|
+
// mistaking this for the public verify and passing `context`) is reported rather than
|
|
449
|
+
// silently swallowed by this function's arity.
|
|
450
|
+
validateVerOpts(opts, INTERNAL_VER_OPT_KEYS);
|
|
426
451
|
const [pkSeed, pubRoot] = publicCoder.decode(publicKey);
|
|
427
|
-
const [random, forsVec, wotsVec] = sigCoder.decode(sig);
|
|
428
452
|
const pk = publicKey;
|
|
453
|
+
// FIPS 205 Algorithm 20 step 1: wrong-length signatures return false instead of throwing
|
|
454
|
+
// (same as ml-dsa). Must run before sigCoder.decode, which throws on length mismatch.
|
|
455
|
+
// Preserve TypeError for non-byte API arguments before treating byte lengths as invalid.
|
|
456
|
+
abytes(sig, undefined, 'signature');
|
|
429
457
|
if (sig.length !== sigCoder.bytesLen)
|
|
430
458
|
return false;
|
|
459
|
+
const [random, forsVec, wotsVec] = sigCoder.decode(sig);
|
|
431
460
|
const context = getContext(pkSeed);
|
|
432
461
|
let { tree, leafIdx, md } = hashMessage(random, pk, msg, context);
|
|
433
462
|
const wotsAddr = setAddr({
|
|
@@ -447,14 +476,16 @@ function gen(opts, hashOpts_) {
|
|
|
447
476
|
const idxOffset = i << A;
|
|
448
477
|
setAddr({ height: 0, index: indices[i] + idxOffset }, forsTreeAddr);
|
|
449
478
|
const leaf = context.thash1(prf, forsTreeAddr);
|
|
450
|
-
//
|
|
451
|
-
|
|
479
|
+
// Copy: computeRoot returns a thashN scratch view, and roots are retained across the
|
|
480
|
+
// remaining FORS iterations (computeRoot itself copies `leaf` before hashing).
|
|
481
|
+
roots.push(copyBytes(computeRoot(leaf, indices[i], idxOffset, authPath, A, context, forsTreeAddr)));
|
|
452
482
|
}
|
|
453
483
|
const forsPkAddr = setAddr({
|
|
454
484
|
type: AddressType.FORSPK,
|
|
455
485
|
keypairAddr: wotsAddr,
|
|
456
486
|
});
|
|
457
|
-
|
|
487
|
+
// Copy: `root` must survive the thash1/thashN calls of the WOTS chain loop below.
|
|
488
|
+
let root = copyBytes(context.thashN(K, concatBytes(...roots), forsPkAddr)); // root = thash()
|
|
458
489
|
// WOTS signature
|
|
459
490
|
const treeAddr = setAddr({ type: AddressType.HASHTREE });
|
|
460
491
|
const wotsPkAddr = setAddr({ type: AddressType.WOTSPK });
|
|
@@ -477,7 +508,8 @@ function gen(opts, hashOpts_) {
|
|
|
477
508
|
}
|
|
478
509
|
}
|
|
479
510
|
const leaf = context.thashN(WOTS_LEN, wotsPk, wotsPkAddr);
|
|
480
|
-
root
|
|
511
|
+
// Copy: `root` is read by chainLengths / equalBytes after later hash calls.
|
|
512
|
+
root = copyBytes(computeRoot(leaf, leafIdx, 0, sigAuth, TREE_HEIGHT, context, treeAddr));
|
|
481
513
|
leafIdx = Number(tree & getMaskBig(TREE_HEIGHT));
|
|
482
514
|
}
|
|
483
515
|
return equalBytes(root, pubRoot);
|
|
@@ -491,14 +523,16 @@ function gen(opts, hashOpts_) {
|
|
|
491
523
|
keygen: internal.keygen,
|
|
492
524
|
getPublicKey: internal.getPublicKey,
|
|
493
525
|
sign: (msg, secretKey, opts = {}) => {
|
|
494
|
-
validateSigOpts(opts);
|
|
526
|
+
opts = validateSigOpts(opts);
|
|
495
527
|
const M = getMessage(msg, opts.context);
|
|
496
|
-
|
|
528
|
+
// `context` is consumed by getMessage() above; forwarding it would make the internal
|
|
529
|
+
// surface accept a key it never reads.
|
|
530
|
+
const res = internal.sign(M, secretKey, { extraEntropy: opts.extraEntropy });
|
|
497
531
|
cleanBytes(M);
|
|
498
532
|
return res;
|
|
499
533
|
},
|
|
500
534
|
verify: (sig, msg, publicKey, opts = {}) => {
|
|
501
|
-
validateVerOpts(opts);
|
|
535
|
+
opts = validateVerOpts(opts);
|
|
502
536
|
return internal.verify(sig, getMessage(msg, opts.context), publicKey);
|
|
503
537
|
},
|
|
504
538
|
prehash: (hash) => {
|
|
@@ -510,14 +544,15 @@ function gen(opts, hashOpts_) {
|
|
|
510
544
|
keygen: internal.keygen,
|
|
511
545
|
getPublicKey: internal.getPublicKey,
|
|
512
546
|
sign: (msg, secretKey, opts = {}) => {
|
|
513
|
-
validateSigOpts(opts);
|
|
547
|
+
opts = validateSigOpts(opts);
|
|
514
548
|
const M = getMessagePrehash(rawHash, msg, opts.context);
|
|
515
|
-
|
|
549
|
+
// As above: getMessagePrehash() consumes `context`, so it must not travel further.
|
|
550
|
+
const res = internal.sign(M, secretKey, { extraEntropy: opts.extraEntropy });
|
|
516
551
|
cleanBytes(M);
|
|
517
552
|
return res;
|
|
518
553
|
},
|
|
519
554
|
verify: (sig, msg, publicKey, opts = {}) => {
|
|
520
|
-
validateVerOpts(opts);
|
|
555
|
+
opts = validateVerOpts(opts);
|
|
521
556
|
return internal.verify(sig, getMessagePrehash(rawHash, msg, opts.context), publicKey);
|
|
522
557
|
},
|
|
523
558
|
});
|
|
@@ -533,21 +568,26 @@ const genShake = () => (opts) => (pubSeed, skSeed) => {
|
|
|
533
568
|
// for each address-bound call instead of reabsorbing the same seed every time.
|
|
534
569
|
const h0 = shake256.create({}).update(pubSeed);
|
|
535
570
|
const h0tmp = h0.clone();
|
|
571
|
+
// Per-context output scratch: thash1/thashN/PRFaddr return these buffers directly, so
|
|
572
|
+
// callers must consume or copy a result before the next call on the same lane.
|
|
573
|
+
const thashOut = new Uint8Array(N);
|
|
574
|
+
const prfOut = new Uint8Array(N);
|
|
536
575
|
const thash = (blocks, input, addr) => {
|
|
537
576
|
stats.thash++;
|
|
538
|
-
|
|
539
|
-
|
|
577
|
+
const len = blocks * N;
|
|
578
|
+
h0._cloneInto(h0tmp)
|
|
540
579
|
.update(addr)
|
|
541
|
-
.update(input.subarray(0,
|
|
542
|
-
.
|
|
580
|
+
.update(input.length === len ? input : input.subarray(0, len))
|
|
581
|
+
.xofInto(thashOut);
|
|
582
|
+
return thashOut;
|
|
543
583
|
};
|
|
544
584
|
return {
|
|
545
585
|
PRFaddr: (addr) => {
|
|
546
586
|
if (!skSeed)
|
|
547
587
|
throw new Error('no sk seed');
|
|
548
588
|
stats.prf++;
|
|
549
|
-
|
|
550
|
-
return
|
|
589
|
+
h0._cloneInto(h0tmp).update(addr).update(skSeed).xofInto(prfOut);
|
|
590
|
+
return prfOut;
|
|
551
591
|
},
|
|
552
592
|
PRFmsg: (skPRF, random, msg) => {
|
|
553
593
|
stats.gen_message_random++;
|
|
@@ -568,11 +608,12 @@ const genShake = () => (opts) => (pubSeed, skSeed) => {
|
|
|
568
608
|
clean: () => {
|
|
569
609
|
h0.destroy();
|
|
570
610
|
h0tmp.destroy();
|
|
611
|
+
cleanBytes(thashOut, prfOut);
|
|
571
612
|
//console.log(stats);
|
|
572
613
|
},
|
|
573
614
|
};
|
|
574
615
|
};
|
|
575
|
-
const SHAKE_SIMPLE = /* @__PURE__ */ (() => ({ getContext: genShake() }))();
|
|
616
|
+
const SHAKE_SIMPLE = /* @__PURE__ */ (() => ({ isCompressed: false, getContext: genShake() }))();
|
|
576
617
|
/**
|
|
577
618
|
* SLH-DSA-SHAKE-128f: Table 2 row `n=16, h=66, d=22, h'=3, a=6, k=33, lg w=4, m=34`;
|
|
578
619
|
* lengths `publicKey=32`, `secretKey=64`, `signature=17088`, `seed=48`, `signRand=16`.
|
|
@@ -635,6 +676,16 @@ const genSha = (h0, h1) => (opts) => (pub_seed, sk_seed) => {
|
|
|
635
676
|
.update(new Uint8Array(h1.blockLen - N));
|
|
636
677
|
const h0tmp = h0ps.clone();
|
|
637
678
|
const h1tmp = h1ps.clone();
|
|
679
|
+
// Per-context output scratch: thash1/thashN/PRFaddr return views into these buffers, so
|
|
680
|
+
// callers must consume or copy a result before the next call on the same lane (see Context
|
|
681
|
+
// docs). digestInto also skips digest()'s per-call destroy(): the tmp states are fully
|
|
682
|
+
// overwritten by the next _cloneInto and wiped in clean().
|
|
683
|
+
const h0out = new Uint8Array(h0.outputLen);
|
|
684
|
+
const h1out = new Uint8Array(h1.outputLen);
|
|
685
|
+
const prfOut = new Uint8Array(h0.outputLen);
|
|
686
|
+
const h0outN = h0out.subarray(0, N);
|
|
687
|
+
const h1outN = h1out.subarray(0, N);
|
|
688
|
+
const prfOutN = prfOut.subarray(0, N);
|
|
638
689
|
// https://www.rfc-editor.org/rfc/rfc8017.html#appendix-B.2.1
|
|
639
690
|
// This local helper is intentionally stricter than generic MGF1 reuse: current SLH-DSA callers
|
|
640
691
|
// only request tiny `m`-byte outputs, but the guard below rejects `length > 2^32` instead of
|
|
@@ -653,27 +704,26 @@ const genSha = (h0, h1) => (opts) => (pub_seed, sk_seed) => {
|
|
|
653
704
|
cleanBytes(out.subarray(length));
|
|
654
705
|
return out.subarray(0, length);
|
|
655
706
|
}
|
|
656
|
-
const thash = (
|
|
707
|
+
const thash = (h, hTmp, out, outN) => (blocks, input, addr) => {
|
|
657
708
|
stats.thash++;
|
|
658
|
-
const
|
|
659
|
-
|
|
709
|
+
const len = blocks * N;
|
|
710
|
+
h._cloneInto(hTmp)
|
|
660
711
|
.update(addr)
|
|
661
|
-
.update(input.subarray(0,
|
|
662
|
-
.
|
|
663
|
-
return
|
|
712
|
+
.update(input.length === len ? input : input.subarray(0, len))
|
|
713
|
+
.digestInto(out);
|
|
714
|
+
return outN;
|
|
664
715
|
};
|
|
665
716
|
return {
|
|
666
717
|
PRFaddr: (addr) => {
|
|
667
718
|
if (!sk_seed)
|
|
668
719
|
throw new Error('No sk seed');
|
|
669
720
|
stats.prf++;
|
|
670
|
-
|
|
721
|
+
h0ps
|
|
671
722
|
._cloneInto(h0tmp)
|
|
672
723
|
.update(addr)
|
|
673
724
|
.update(sk_seed)
|
|
674
|
-
.
|
|
675
|
-
|
|
676
|
-
return res;
|
|
725
|
+
.digestInto(prfOut);
|
|
726
|
+
return prfOutN;
|
|
677
727
|
},
|
|
678
728
|
PRFmsg: (skPRF, random, msg) => {
|
|
679
729
|
stats.gen_message_random++;
|
|
@@ -689,13 +739,14 @@ const genSha = (h0, h1) => (opts) => (pub_seed, sk_seed) => {
|
|
|
689
739
|
const seed = concatBytes(R.subarray(0, N), pk.subarray(0, N), h1.create().update(R.subarray(0, N)).update(pk).update(m).digest());
|
|
690
740
|
return mgf1(seed, outLen, h1);
|
|
691
741
|
},
|
|
692
|
-
thash1: thash(
|
|
693
|
-
thashN: thash(
|
|
742
|
+
thash1: thash(h0ps, h0tmp, h0out, h0outN).bind(null, 1),
|
|
743
|
+
thashN: thash(h1ps, h1tmp, h1out, h1outN),
|
|
694
744
|
clean: () => {
|
|
695
745
|
h0ps.destroy();
|
|
696
746
|
h1ps.destroy();
|
|
697
747
|
h0tmp.destroy();
|
|
698
748
|
h1tmp.destroy();
|
|
749
|
+
cleanBytes(h0out, h1out, prfOut);
|
|
699
750
|
//console.log(stats);
|
|
700
751
|
},
|
|
701
752
|
};
|
|
@@ -712,6 +763,23 @@ const SHA512_SIMPLE = /* @__PURE__ */ (() => ({
|
|
|
712
763
|
* 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
764
|
* lengths `publicKey=32`, `secretKey=64`, `signature=17088`, `seed=48`, `signRand=16`.
|
|
714
765
|
* Also exposes `.prehash(...)`.
|
|
766
|
+
* @example
|
|
767
|
+
* Generate deterministic SLH-DSA keys, sign one message, and verify the signature.
|
|
768
|
+
* ```ts
|
|
769
|
+
* import { sha256 } from '@noble/hashes/sha2.js';
|
|
770
|
+
* import { slh_dsa_sha2_128f } from '@noble/post-quantum/slh-dsa.js';
|
|
771
|
+
* const seed = new Uint8Array(slh_dsa_sha2_128f.lengths.seed!);
|
|
772
|
+
* const { secretKey, publicKey } = slh_dsa_sha2_128f.keygen(seed);
|
|
773
|
+
* const msg = new TextEncoder().encode('hello noble');
|
|
774
|
+
* const sig = slh_dsa_sha2_128f.sign(msg, secretKey);
|
|
775
|
+
* const isValid = slh_dsa_sha2_128f.verify(sig, msg, publicKey);
|
|
776
|
+
* const recovered = slh_dsa_sha2_128f.getPublicKey(secretKey);
|
|
777
|
+
* const context = new Uint8Array([1, 2, 3]);
|
|
778
|
+
* const prehash = slh_dsa_sha2_128f.prehash(sha256);
|
|
779
|
+
* const preSig = prehash.sign(msg, secretKey, { context });
|
|
780
|
+
* const preValid = prehash.verify(preSig, msg, publicKey, { context });
|
|
781
|
+
* const internalSig = slh_dsa_sha2_128f.internal.sign(msg, secretKey);
|
|
782
|
+
* ```
|
|
715
783
|
*/
|
|
716
784
|
export const slh_dsa_sha2_128f = /* @__PURE__ */ (() => gen(PARAMS['128f'], SHA256_SIMPLE))();
|
|
717
785
|
/**
|
|
@@ -744,4 +812,3 @@ export const slh_dsa_sha2_256f = /* @__PURE__ */ (() => gen(PARAMS['256f'], SHA5
|
|
|
744
812
|
* Also exposes `.prehash(...)`.
|
|
745
813
|
*/
|
|
746
814
|
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>;
|
|
@@ -106,7 +112,7 @@ export const genCrystals = <T extends TypedArray>(opts: CrystalOpts<T>): TRet<Cr
|
|
|
106
112
|
// Normalize JS `%` into the canonical Z_m representative `[0, modulo-1]` expected by
|
|
107
113
|
// FIPS 203 §2.3 / FIPS 204 §2.3 before downstream mod-q arithmetic.
|
|
108
114
|
const mod = (a: number, modulo = Q): number => {
|
|
109
|
-
const result = a % modulo | 0;
|
|
115
|
+
const result = (a % modulo) | 0;
|
|
110
116
|
return (result >= 0 ? result | 0 : (modulo + result) | 0) | 0;
|
|
111
117
|
};
|
|
112
118
|
// FIPS 204 §7.4 uses the centered `mod ±` representative for low bits, keeping the
|
|
@@ -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
|
},
|