@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/ml-dsa.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CHash } from '@noble/hashes/utils.js';
|
|
1
2
|
import { type CryptoKeys, type Signer, type SigOpts, type TArg, type TRet, type VerOpts } from './utils.ts';
|
|
2
3
|
/** Internal ML-DSA options. */
|
|
3
4
|
export type DSAInternalOpts = {
|
|
@@ -17,6 +18,13 @@ export type DSAInternal = CryptoKeys & {
|
|
|
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
|
package/ml-dsa.js
CHANGED
|
@@ -72,6 +72,8 @@ const polyCoder = (d, compress = id, verify = id) => crystals.bitsCoder(d, {
|
|
|
72
72
|
decode: (i) => verify(compress(i)),
|
|
73
73
|
});
|
|
74
74
|
// Mutates `a` in place; callers must pass same-length polynomials.
|
|
75
|
+
// NOTE: conditional-reduction variants (as in ml-kem) were measured performance-neutral here —
|
|
76
|
+
// int32 `%` with 23-bit Q is already cheap — so the simpler mod() form is kept for audit.
|
|
75
77
|
const polyAdd = (a_, b_) => {
|
|
76
78
|
const a = a_;
|
|
77
79
|
const b = b_;
|
|
@@ -179,14 +181,16 @@ function getDilithium(opts_) {
|
|
|
179
181
|
// See dilithium-py README section "Optimising decomposition and making hints".
|
|
180
182
|
return res0;
|
|
181
183
|
};
|
|
184
|
+
// m = (q-1)/(2γ2): 44 for ML-DSA-44, 16 for 65/87. Hoisted out of UseHint, which runs
|
|
185
|
+
// per coefficient during verification.
|
|
186
|
+
const HINT_M = Math.floor((Q - 1) / (2 * GAMMA2));
|
|
182
187
|
const UseHint = (h, r) => {
|
|
183
188
|
// Returns the high bits of r adjusted according to hint h
|
|
184
|
-
const m = Math.floor((Q - 1) / (2 * GAMMA2));
|
|
185
189
|
const { r1, r0 } = decompose(r);
|
|
186
190
|
// 3: if h = 1 and r0 > 0 return (r1 + 1) mod m
|
|
187
191
|
// 4: if h = 1 and r0 ≤ 0 return (r1 − 1) mod m
|
|
188
192
|
if (h === 1)
|
|
189
|
-
return r0 > 0 ? crystals.mod(r1 + 1,
|
|
193
|
+
return r0 > 0 ? crystals.mod(r1 + 1, HINT_M) | 0 : crystals.mod(r1 - 1, HINT_M) | 0;
|
|
190
194
|
return r1 | 0;
|
|
191
195
|
};
|
|
192
196
|
const Power2Round = (r) => {
|
|
@@ -421,11 +425,34 @@ function getDilithium(opts_) {
|
|
|
421
425
|
sign: (msg, secretKey, opts = {}) => {
|
|
422
426
|
validateSigOpts(opts);
|
|
423
427
|
validateInternalOpts(opts);
|
|
424
|
-
|
|
428
|
+
const { extraEntropy: random, externalMu = false } = opts;
|
|
429
|
+
// FIPS 204 external-mu mode expects the 64-byte message representative µ = H(tr || M).
|
|
430
|
+
if (externalMu)
|
|
431
|
+
abytes(msg, CRH_BYTES, 'mu');
|
|
432
|
+
// Prepare entropy before touching decoded secrets: randomBytes() may throw, and an RNG
|
|
433
|
+
// failure must not leave expanded secret-polynomial copies behind.
|
|
434
|
+
const ownRnd = random === false || random === undefined;
|
|
435
|
+
const rnd = random === false
|
|
436
|
+
? new Uint8Array(32)
|
|
437
|
+
: random === undefined
|
|
438
|
+
? randomBytes(signRandBytes)
|
|
439
|
+
: random;
|
|
440
|
+
abytes(rnd, 32, 'extraEntropy');
|
|
425
441
|
// This part can be pre-cached per secretKey, but there is only minor performance improvement,
|
|
426
442
|
// since we re-use a lot of variables to computation.
|
|
427
443
|
// (ρ, K,tr, s1, s2, t0) ← skDecode(sk)
|
|
428
|
-
const
|
|
444
|
+
const decoded = (() => {
|
|
445
|
+
try {
|
|
446
|
+
return secretCoder.decode(secretKey);
|
|
447
|
+
}
|
|
448
|
+
catch (error) {
|
|
449
|
+
// A malformed key must not strand entropy owned by the library.
|
|
450
|
+
if (ownRnd)
|
|
451
|
+
cleanBytes(rnd);
|
|
452
|
+
throw error;
|
|
453
|
+
}
|
|
454
|
+
})();
|
|
455
|
+
const [rho, _K, tr, s1, s2, t0] = decoded;
|
|
429
456
|
// Cache matrix to avoid re-compute later
|
|
430
457
|
const A = []; // A ← ExpandA(ρ)
|
|
431
458
|
const xof = XOF128(rho);
|
|
@@ -448,19 +475,15 @@ function getDilithium(opts_) {
|
|
|
448
475
|
: // 6: µ ← H(tr||M, 512)
|
|
449
476
|
// ▷ Compute message representative µ
|
|
450
477
|
shake256.create({ dkLen: CRH_BYTES }).update(tr).update(msg).digest();
|
|
451
|
-
// Compute private random seed
|
|
452
|
-
const rnd = random === false
|
|
453
|
-
? new Uint8Array(32)
|
|
454
|
-
: random === undefined
|
|
455
|
-
? randomBytes(signRandBytes)
|
|
456
|
-
: random;
|
|
457
|
-
abytes(rnd, 32, 'extraEntropy');
|
|
458
478
|
const rhoprime = shake256
|
|
459
479
|
.create({ dkLen: CRH_BYTES })
|
|
460
480
|
.update(_K)
|
|
461
481
|
.update(rnd)
|
|
462
482
|
.update(mu)
|
|
463
483
|
.digest(); // ρ′← H(K||rnd||µ, 512)
|
|
484
|
+
// Only wipe entropy we generated; caller-provided extraEntropy stays caller-owned.
|
|
485
|
+
if (ownRnd)
|
|
486
|
+
cleanBytes(rnd);
|
|
464
487
|
abytes(rhoprime, CRH_BYTES);
|
|
465
488
|
const x256 = XOF256(rhoprime, ZCoder.bytesLen);
|
|
466
489
|
// Rejection sampling loop
|
|
@@ -532,6 +555,9 @@ function getDilithium(opts_) {
|
|
|
532
555
|
verify: (sig, msg, publicKey, opts = {}) => {
|
|
533
556
|
validateInternalOpts(opts);
|
|
534
557
|
const { externalMu = false } = opts;
|
|
558
|
+
// FIPS 204 external-mu mode expects the 64-byte message representative µ = H(tr || M).
|
|
559
|
+
if (externalMu)
|
|
560
|
+
abytes(msg, CRH_BYTES, 'mu');
|
|
535
561
|
// ML-DSA.Verify(pk, M, σ): Verifes a signature σ for a message M.
|
|
536
562
|
const [rho, t1] = publicCoder.decode(publicKey); // (ρ, t1) ← pkDecode(pk)
|
|
537
563
|
const tr = shake256(publicKey, { dkLen: TR_BYTES }); // 6: tr ← H(BytesToBits(pk), 512)
|
|
@@ -604,10 +630,12 @@ function getDilithium(opts_) {
|
|
|
604
630
|
},
|
|
605
631
|
verify: (sig, msg, publicKey, opts = {}) => {
|
|
606
632
|
validateVerOpts(opts);
|
|
633
|
+
abytes(sig, undefined, 'signature');
|
|
607
634
|
return internal.verify(sig, getMessage(msg, opts.context), publicKey);
|
|
608
635
|
},
|
|
609
636
|
prehash: (hash) => {
|
|
610
637
|
checkHash(hash, securityLevel);
|
|
638
|
+
const rawHash = hash;
|
|
611
639
|
return Object.freeze({
|
|
612
640
|
info: Object.freeze({ type: 'hashml-dsa' }),
|
|
613
641
|
securityLevel: securityLevel,
|
|
@@ -616,20 +644,40 @@ function getDilithium(opts_) {
|
|
|
616
644
|
getPublicKey: internal.getPublicKey,
|
|
617
645
|
sign: (msg, secretKey, opts = {}) => {
|
|
618
646
|
validateSigOpts(opts);
|
|
619
|
-
const M = getMessagePrehash(
|
|
647
|
+
const M = getMessagePrehash(rawHash, msg, opts.context);
|
|
620
648
|
const res = internal.sign(M, secretKey, opts);
|
|
621
649
|
cleanBytes(M);
|
|
622
650
|
return res;
|
|
623
651
|
},
|
|
624
652
|
verify: (sig, msg, publicKey, opts = {}) => {
|
|
625
653
|
validateVerOpts(opts);
|
|
626
|
-
|
|
654
|
+
abytes(sig, undefined, 'signature');
|
|
655
|
+
return internal.verify(sig, getMessagePrehash(rawHash, msg, opts.context), publicKey);
|
|
627
656
|
},
|
|
628
657
|
});
|
|
629
658
|
},
|
|
630
659
|
});
|
|
631
660
|
}
|
|
632
|
-
/**
|
|
661
|
+
/**
|
|
662
|
+
* ML-DSA-44 for 128-bit security level. Not recommended after 2030, as per ASD.
|
|
663
|
+
* @example
|
|
664
|
+
* Generate deterministic ML-DSA-44 keys, sign one message, and verify the signature.
|
|
665
|
+
* ```ts
|
|
666
|
+
* import { sha256 } from '@noble/hashes/sha2.js';
|
|
667
|
+
* import { ml_dsa44 } from '@noble/post-quantum/ml-dsa.js';
|
|
668
|
+
* const seed = new Uint8Array(ml_dsa44.lengths.seed!);
|
|
669
|
+
* const { secretKey, publicKey } = ml_dsa44.keygen(seed);
|
|
670
|
+
* const msg = new TextEncoder().encode('hello noble');
|
|
671
|
+
* const sig = ml_dsa44.sign(msg, secretKey);
|
|
672
|
+
* const isValid = ml_dsa44.verify(sig, msg, publicKey);
|
|
673
|
+
* const recovered = ml_dsa44.getPublicKey(secretKey);
|
|
674
|
+
* const context = new Uint8Array([1, 2, 3]);
|
|
675
|
+
* const prehash = ml_dsa44.prehash(sha256);
|
|
676
|
+
* const preSig = prehash.sign(msg, secretKey, { context });
|
|
677
|
+
* const preValid = prehash.verify(preSig, msg, publicKey, { context });
|
|
678
|
+
* const internalSig = ml_dsa44.internal.sign(msg, secretKey);
|
|
679
|
+
* ```
|
|
680
|
+
*/
|
|
633
681
|
export const ml_dsa44 = /* @__PURE__ */ (() => getDilithium({
|
|
634
682
|
...PARAMS[2],
|
|
635
683
|
CRH_BYTES: 64,
|
|
@@ -659,4 +707,3 @@ export const ml_dsa87 = /* @__PURE__ */ (() => getDilithium({
|
|
|
659
707
|
XOF256,
|
|
660
708
|
securityLevel: 256,
|
|
661
709
|
}))();
|
|
662
|
-
//# sourceMappingURL=ml-dsa.js.map
|
package/ml-kem.d.ts
CHANGED
|
@@ -26,20 +26,61 @@ export type KEMParam = {
|
|
|
26
26
|
* not a generic security label.
|
|
27
27
|
*/
|
|
28
28
|
export declare const PARAMS: Record<string, KEMParam>;
|
|
29
|
+
/**
|
|
30
|
+
* Prepared (pre-expanded) ML-KEM public key. Experimental prototype.
|
|
31
|
+
* Caches only public data: packed ek, the expanded matrix Â, decoded t̂ and H(ek). No secret
|
|
32
|
+
* material is retained between calls; secret keys passed to `decapsulate` are decoded and wiped
|
|
33
|
+
* per call, exactly like the one-shot API. `clean()` wipes the expanded Â/t̂ cache; the packed
|
|
34
|
+
* public key and H(ek) are public and are not wiped. The object must not be used afterwards.
|
|
35
|
+
*/
|
|
36
|
+
export type KEMPrepared = {
|
|
37
|
+
/**
|
|
38
|
+
* Detached copy of the source public key. Treat as read-only while the prepared object is in use.
|
|
39
|
+
* Callers may wipe it after final use; any mutation invalidates subsequent operations.
|
|
40
|
+
*/
|
|
41
|
+
publicKey: Uint8Array;
|
|
42
|
+
/** Same as `KEM.encapsulate`, minus per-call ek re-validation and  re-expansion. */
|
|
43
|
+
encapsulate: (msg?: Uint8Array) => {
|
|
44
|
+
cipherText: Uint8Array;
|
|
45
|
+
sharedSecret: Uint8Array;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Same as `KEM.decapsulate`; throws if `secretKey` does not embed this public key.
|
|
49
|
+
* The embedded-ek byte comparison plus stored-hash comparison is equivalent to the
|
|
50
|
+
* FIPS 203 §7.3 hash input check.
|
|
51
|
+
*/
|
|
52
|
+
decapsulate: (cipherText: Uint8Array, secretKey: Uint8Array) => Uint8Array;
|
|
53
|
+
/** Wipe cached (public) data. */
|
|
54
|
+
clean: () => void;
|
|
55
|
+
};
|
|
56
|
+
/** KEM with prepared-key support. */
|
|
57
|
+
export type MLKEM = KEM & {
|
|
58
|
+
prepare: (publicKey: Uint8Array) => KEMPrepared;
|
|
59
|
+
};
|
|
29
60
|
/**
|
|
30
61
|
* ML-KEM-512: Table 2 row `k=2, η1=3, η2=2, du=10, dv=4`; Table 3 sizes `800/1632/768/32`.
|
|
31
62
|
* The ASD lifecycle note here is external policy guidance, not a FIPS 203 requirement.
|
|
63
|
+
* @example
|
|
64
|
+
* Generate deterministic ML-KEM-512 keys, encapsulate a shared secret, and decapsulate it.
|
|
65
|
+
* ```ts
|
|
66
|
+
* import { ml_kem512 } from '@noble/post-quantum/ml-kem.js';
|
|
67
|
+
* const seed = new Uint8Array(ml_kem512.lengths.seed!);
|
|
68
|
+
* const { secretKey, publicKey } = ml_kem512.keygen(seed);
|
|
69
|
+
* const msg = new Uint8Array(ml_kem512.lengths.msgRand!);
|
|
70
|
+
* const { cipherText, sharedSecret } = ml_kem512.encapsulate(publicKey, msg);
|
|
71
|
+
* const recovered = ml_kem512.decapsulate(cipherText, secretKey);
|
|
72
|
+
* const publicKey2 = ml_kem512.getPublicKey(secretKey);
|
|
73
|
+
* ```
|
|
32
74
|
*/
|
|
33
|
-
export declare const ml_kem512: TRet<
|
|
75
|
+
export declare const ml_kem512: TRet<MLKEM>;
|
|
34
76
|
/**
|
|
35
77
|
* ML-KEM-768: Table 2 row `k=3, η1=2, η2=2, du=10, dv=4`; Table 3 sizes `1184/2400/1088/32`.
|
|
36
78
|
* The ASD lifecycle note here is external policy guidance, not a FIPS 203 requirement.
|
|
37
79
|
*/
|
|
38
|
-
export declare const ml_kem768: TRet<
|
|
80
|
+
export declare const ml_kem768: TRet<MLKEM>;
|
|
39
81
|
/**
|
|
40
82
|
* ML-KEM-1024: Table 2 row `k=4, η1=2, η2=2, du=11, dv=5`; Table 3 sizes `1568/3168/1568/32`.
|
|
41
83
|
* The ASD lifecycle note here is external policy guidance, not a FIPS 203 requirement.
|
|
42
84
|
*/
|
|
43
|
-
export declare const ml_kem1024: TRet<
|
|
85
|
+
export declare const ml_kem1024: TRet<MLKEM>;
|
|
44
86
|
export declare const __tests: any;
|
|
45
|
-
//# sourceMappingURL=ml-kem.d.ts.map
|
package/ml-kem.js
CHANGED
|
@@ -84,24 +84,32 @@ const byteCoder = (d) => crystals.bitsCoder(d, d === 12
|
|
|
84
84
|
// Kinda like convertRadix2 from @scure/base.
|
|
85
85
|
// decode(encode(t)) == t, but there is loss of information on encode(decode(t))
|
|
86
86
|
const polyCoder = (d) => (d === 12 ? byteCoder(12) : crystals.bitsCoder(d, compress(d)));
|
|
87
|
+
// Coefficients always stay reduced in [0, Q) here (samplers, NTT and coders all reduce),
|
|
88
|
+
// so one conditional correction replaces the generic mod().
|
|
87
89
|
function polyAdd(a_, b_) {
|
|
88
90
|
const a = a_;
|
|
89
91
|
const b = b_;
|
|
90
92
|
// Mutates `a` in place; callers must pass two N=256 polynomials.
|
|
91
|
-
for (let i = 0; i < N; i++)
|
|
92
|
-
|
|
93
|
+
for (let i = 0; i < N; i++) {
|
|
94
|
+
const r = a[i] + b[i]; // a += b
|
|
95
|
+
a[i] = r >= Q ? r - Q : r;
|
|
96
|
+
}
|
|
93
97
|
}
|
|
94
98
|
function polySub(a_, b_) {
|
|
95
99
|
const a = a_;
|
|
96
100
|
const b = b_;
|
|
97
101
|
// Mutates `a` in place; callers must pass two N=256 polynomials.
|
|
98
|
-
for (let i = 0; i < N; i++)
|
|
99
|
-
|
|
102
|
+
for (let i = 0; i < N; i++) {
|
|
103
|
+
const r = a[i] - b[i]; // a -= b
|
|
104
|
+
a[i] = r < 0 ? r + Q : r;
|
|
105
|
+
}
|
|
100
106
|
}
|
|
101
107
|
// FIPS-203: Computes the product of two degree-one polynomials with respect to a quadratic modulus
|
|
102
108
|
function BaseCaseMultiply(a0, a1, b0, b1, zeta) {
|
|
103
109
|
// `zeta` here is Algorithm 11's γ = ζ^(2BitRev_7(i)+1).
|
|
104
|
-
|
|
110
|
+
// Reduce a1*b1 before multiplying by zeta: a1*b1*zeta would reach ~2^35, forcing JS engines
|
|
111
|
+
// into slow float fmod; with the extra reduction every intermediate fits int32.
|
|
112
|
+
const c0 = crystals.mod(crystals.mod(a1 * b1) * zeta + a0 * b0);
|
|
105
113
|
const c1 = crystals.mod(a0 * b1 + a1 * b0);
|
|
106
114
|
return { c0, c1 };
|
|
107
115
|
}
|
|
@@ -191,6 +199,34 @@ const genKPKE = (opts_) => {
|
|
|
191
199
|
const secretCoder = vecCoder(polyCoder(12), K);
|
|
192
200
|
const cipherCoder = splitCoder('ciphertext', vecCoder(polyU, K), polyV);
|
|
193
201
|
const seedCoder = splitCoder('seed', 32, 32);
|
|
202
|
+
// Algorithm 14 (K-PKE.Encrypt) core, after ek parsing. `tHat` and every poly returned by
|
|
203
|
+
// `getA(i, j)` are treated as disposable scratch: they are mutated in place and wiped/dropped,
|
|
204
|
+
// so callers holding cached copies must pass fresh copies.
|
|
205
|
+
const encryptCore = (tHat, getA, msg, seed) => {
|
|
206
|
+
const rHat = [];
|
|
207
|
+
for (let i = 0; i < K; i++)
|
|
208
|
+
rHat.push(crystals.NTT.encode(sampleCBD(PRF, seed, i, ETA1)));
|
|
209
|
+
const tmp2 = new Uint16Array(N);
|
|
210
|
+
const u = [];
|
|
211
|
+
for (let i = 0; i < K; i++) {
|
|
212
|
+
const e1 = sampleCBD(PRF, seed, K + i, ETA2);
|
|
213
|
+
const tmp = new Uint16Array(N);
|
|
214
|
+
for (let j = 0; j < K; j++) {
|
|
215
|
+
const aij = getA(i, j); // A[j][i], inplace transpose access
|
|
216
|
+
polyAdd(tmp, MultiplyNTTs(aij, rHat[j])); // t += aij * rHat[j]
|
|
217
|
+
}
|
|
218
|
+
polyAdd(e1, crystals.NTT.decode(tmp)); // e1 += tmp
|
|
219
|
+
u.push(e1);
|
|
220
|
+
polyAdd(tmp2, MultiplyNTTs(tHat[i], rHat[i])); // t2 += tHat[i] * rHat[i]
|
|
221
|
+
cleanBytes(tmp);
|
|
222
|
+
}
|
|
223
|
+
const e2 = sampleCBD(PRF, seed, 2 * K, ETA2);
|
|
224
|
+
polyAdd(e2, crystals.NTT.decode(tmp2)); // e2 += tmp2
|
|
225
|
+
const v = poly1.decode(msg); // encode plaintext m into polynomial v
|
|
226
|
+
polyAdd(v, e2); // v += e2
|
|
227
|
+
cleanBytes(tHat, rHat, tmp2, e2);
|
|
228
|
+
return cipherCoder.encode([u, v]);
|
|
229
|
+
};
|
|
194
230
|
return {
|
|
195
231
|
secretCoder,
|
|
196
232
|
lengths: {
|
|
@@ -231,31 +267,26 @@ const genKPKE = (opts_) => {
|
|
|
231
267
|
},
|
|
232
268
|
encrypt: (publicKey, msg, seed) => {
|
|
233
269
|
const [tHat, rho] = publicCoder.decode(publicKey);
|
|
234
|
-
const rHat = [];
|
|
235
|
-
for (let i = 0; i < K; i++)
|
|
236
|
-
rHat.push(crystals.NTT.encode(sampleCBD(PRF, seed, i, ETA1)));
|
|
237
270
|
const x = XOF(rho);
|
|
238
|
-
const
|
|
239
|
-
const u = [];
|
|
240
|
-
for (let i = 0; i < K; i++) {
|
|
241
|
-
const e1 = sampleCBD(PRF, seed, K + i, ETA2);
|
|
242
|
-
const tmp = new Uint16Array(N);
|
|
243
|
-
for (let j = 0; j < K; j++) {
|
|
244
|
-
const aij = SampleNTT(x.get(i, j)); // A[j][i], inplace transpose access
|
|
245
|
-
polyAdd(tmp, MultiplyNTTs(aij, rHat[j])); // t += aij * rHat[j]
|
|
246
|
-
}
|
|
247
|
-
polyAdd(e1, crystals.NTT.decode(tmp)); // e1 += tmp
|
|
248
|
-
u.push(e1);
|
|
249
|
-
polyAdd(tmp2, MultiplyNTTs(tHat[i], rHat[i])); // t2 += tHat[i] * rHat[i]
|
|
250
|
-
cleanBytes(tmp);
|
|
251
|
-
}
|
|
271
|
+
const res = encryptCore(tHat, (i, j) => SampleNTT(x.get(i, j)), msg, seed);
|
|
252
272
|
x.clean();
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
273
|
+
return res;
|
|
274
|
+
},
|
|
275
|
+
// Expands the full  matrix (public data derived from rho) once, so repeated encryptions
|
|
276
|
+
// against the same ek skip the K² SampleNTT XOF expansions. Cached polys are copied per
|
|
277
|
+
// call because encryptCore mutates its inputs in place.
|
|
278
|
+
prepare: (publicKey) => {
|
|
279
|
+
const [tHat, rho] = publicCoder.decode(publicKey);
|
|
280
|
+
const x = XOF(rho);
|
|
281
|
+
const A = [];
|
|
282
|
+
for (let i = 0; i < K; i++)
|
|
283
|
+
for (let j = 0; j < K; j++)
|
|
284
|
+
A.push(SampleNTT(x.get(i, j)));
|
|
285
|
+
x.clean();
|
|
286
|
+
return {
|
|
287
|
+
encrypt: (msg, seed) => encryptCore(tHat.map((p) => p.slice()), (i, j) => A[i * K + j].slice(), msg, seed),
|
|
288
|
+
clean: () => cleanBytes(tHat, A),
|
|
289
|
+
};
|
|
259
290
|
},
|
|
260
291
|
decrypt: (cipherText, privateKey) => {
|
|
261
292
|
const [u, v] = cipherCoder.decode(cipherText);
|
|
@@ -287,6 +318,18 @@ function createKyber(opts) {
|
|
|
287
318
|
const secretCoder = splitCoder('secretKey', lengths.secretKey, lengths.publicKey, 32, 32);
|
|
288
319
|
const msgLen = 32;
|
|
289
320
|
const seedLen = 64;
|
|
321
|
+
// FIPS-203 includes additional verification check for modulus
|
|
322
|
+
const validateModulus = (publicKey, fn) => {
|
|
323
|
+
const eke = publicKey.subarray(0, 384 * rawOpts.K);
|
|
324
|
+
// Copy because of inplace encoding
|
|
325
|
+
const ek = KPKESecretCoder.encode(KPKESecretCoder.decode(copyBytes(eke)));
|
|
326
|
+
// (Modulus check.) Perform the computation ek ← ByteEncode12(ByteDecode12(eke)).
|
|
327
|
+
// If ek = ̸ eke, the input is invalid. (See Section 4.2.1.)
|
|
328
|
+
const ok = equalBytes(ek, eke);
|
|
329
|
+
cleanBytes(ek);
|
|
330
|
+
if (!ok)
|
|
331
|
+
throw new Error(`ML-KEM.${fn}: wrong publicKey modulus`);
|
|
332
|
+
};
|
|
290
333
|
const kemLengths = Object.freeze({
|
|
291
334
|
...lengths,
|
|
292
335
|
seed: 64,
|
|
@@ -316,17 +359,7 @@ function createKyber(opts) {
|
|
|
316
359
|
encapsulate: (publicKey, msg = randomBytes(msgLen)) => {
|
|
317
360
|
abytes(publicKey, lengths.publicKey, 'publicKey');
|
|
318
361
|
abytes(msg, msgLen, 'message');
|
|
319
|
-
|
|
320
|
-
const eke = publicKey.subarray(0, 384 * opts.K);
|
|
321
|
-
// Copy because of inplace encoding
|
|
322
|
-
const ek = KPKESecretCoder.encode(KPKESecretCoder.decode(copyBytes(eke)));
|
|
323
|
-
// (Modulus check.) Perform the computation ek ← ByteEncode12(ByteDecode12(eke)).
|
|
324
|
-
// If ek = ̸ eke, the input is invalid. (See Section 4.2.1.)
|
|
325
|
-
if (!equalBytes(ek, eke)) {
|
|
326
|
-
cleanBytes(ek);
|
|
327
|
-
throw new Error('ML-KEM.encapsulate: wrong publicKey modulus');
|
|
328
|
-
}
|
|
329
|
-
cleanBytes(ek);
|
|
362
|
+
validateModulus(publicKey, 'encapsulate');
|
|
330
363
|
// derive randomness
|
|
331
364
|
const kr = HASH512.create().update(msg).update(HASH256(publicKey)).digest();
|
|
332
365
|
const cipherText = KPKE.encrypt(publicKey, msg, kr.subarray(32, 64));
|
|
@@ -356,9 +389,57 @@ function createKyber(opts) {
|
|
|
356
389
|
// if ciphertexts do not match, “implicitly reject”
|
|
357
390
|
const isValid = equalBytes(cipherText, cipherText2);
|
|
358
391
|
const Kbar = KDF.create({ dkLen: 32 }).update(z).update(cipherText).digest();
|
|
359
|
-
|
|
392
|
+
// kr[32:64] is the derived K-PKE encryption randomness: wipe it like encapsulate() does.
|
|
393
|
+
cleanBytes(msg, cipherText2, kr.subarray(32), !isValid ? Khat : Kbar);
|
|
360
394
|
return (isValid ? Khat : Kbar);
|
|
361
395
|
},
|
|
396
|
+
/**
|
|
397
|
+
* Experimental prototype: pre-expand a public key so repeated encapsulate/decapsulate
|
|
398
|
+
* against the same key skip re-validation, H(ek), t̂ decoding and the K² SampleNTT
|
|
399
|
+
* XOF expansions of Â. Only public data is cached; see {@link KEMPrepared}.
|
|
400
|
+
*/
|
|
401
|
+
prepare: (publicKey) => {
|
|
402
|
+
abytes(publicKey, lengths.publicKey, 'publicKey');
|
|
403
|
+
validateModulus(publicKey, 'prepare');
|
|
404
|
+
const ek = copyBytes(publicKey); // detach from the caller before caching
|
|
405
|
+
const publicKeyHash = HASH256(ek);
|
|
406
|
+
const cached = KPKE.prepare(ek);
|
|
407
|
+
return Object.freeze({
|
|
408
|
+
publicKey: ek,
|
|
409
|
+
encapsulate: (msg = randomBytes(msgLen)) => {
|
|
410
|
+
abytes(msg, msgLen, 'message');
|
|
411
|
+
const kr = HASH512.create().update(msg).update(publicKeyHash).digest();
|
|
412
|
+
const cipherText = cached.encrypt(msg, kr.subarray(32, 64));
|
|
413
|
+
cleanBytes(kr.subarray(32));
|
|
414
|
+
return {
|
|
415
|
+
cipherText: cipherText,
|
|
416
|
+
sharedSecret: kr.subarray(0, 32),
|
|
417
|
+
};
|
|
418
|
+
},
|
|
419
|
+
decapsulate: (cipherText, secretKey) => {
|
|
420
|
+
abytes(secretKey, secretCoder.bytesLen, 'secretKey');
|
|
421
|
+
abytes(cipherText, lengths.cipherText, 'cipherText');
|
|
422
|
+
const [sk, ekEmbedded, storedHash, z] = secretCoder.decode(secretKey);
|
|
423
|
+
// Under KEMPrepared's read-only publicKey contract, bind dk to the prepared key.
|
|
424
|
+
// Together with publicKeyHash = H(ek) computed in prepare(), this is equivalent to (and
|
|
425
|
+
// stronger than) FIPS 203 §7.3's `H(dk[384k : 768k+32]) == dk[768k+32 : 768k+64]`.
|
|
426
|
+
if (!equalBytes(ekEmbedded, ek) || !equalBytes(storedHash, publicKeyHash))
|
|
427
|
+
throw new Error('ML-KEM.decapsulate: secretKey does not match prepared publicKey');
|
|
428
|
+
const msg = KPKE.decrypt(cipherText, sk);
|
|
429
|
+
// derive randomness, Khat, rHat = G(mHat || h)
|
|
430
|
+
const kr = HASH512.create().update(msg).update(publicKeyHash).digest();
|
|
431
|
+
const Khat = kr.subarray(0, 32);
|
|
432
|
+
// re-encrypt using the derived randomness and cached Â/t̂
|
|
433
|
+
const cipherText2 = cached.encrypt(msg, kr.subarray(32, 64));
|
|
434
|
+
// if ciphertexts do not match, “implicitly reject”
|
|
435
|
+
const isValid = equalBytes(cipherText, cipherText2);
|
|
436
|
+
const Kbar = KDF.create({ dkLen: 32 }).update(z).update(cipherText).digest();
|
|
437
|
+
cleanBytes(msg, cipherText2, kr.subarray(32), !isValid ? Khat : Kbar);
|
|
438
|
+
return (isValid ? Khat : Kbar);
|
|
439
|
+
},
|
|
440
|
+
clean: cached.clean,
|
|
441
|
+
});
|
|
442
|
+
},
|
|
362
443
|
});
|
|
363
444
|
}
|
|
364
445
|
// FIPS 203's PRF_eta binding: current callers use only 32-byte keys, one-byte nonces,
|
|
@@ -388,6 +469,17 @@ const mk = (params) => createKyber({
|
|
|
388
469
|
/**
|
|
389
470
|
* ML-KEM-512: Table 2 row `k=2, η1=3, η2=2, du=10, dv=4`; Table 3 sizes `800/1632/768/32`.
|
|
390
471
|
* The ASD lifecycle note here is external policy guidance, not a FIPS 203 requirement.
|
|
472
|
+
* @example
|
|
473
|
+
* Generate deterministic ML-KEM-512 keys, encapsulate a shared secret, and decapsulate it.
|
|
474
|
+
* ```ts
|
|
475
|
+
* import { ml_kem512 } from '@noble/post-quantum/ml-kem.js';
|
|
476
|
+
* const seed = new Uint8Array(ml_kem512.lengths.seed!);
|
|
477
|
+
* const { secretKey, publicKey } = ml_kem512.keygen(seed);
|
|
478
|
+
* const msg = new Uint8Array(ml_kem512.lengths.msgRand!);
|
|
479
|
+
* const { cipherText, sharedSecret } = ml_kem512.encapsulate(publicKey, msg);
|
|
480
|
+
* const recovered = ml_kem512.decapsulate(cipherText, secretKey);
|
|
481
|
+
* const publicKey2 = ml_kem512.getPublicKey(secretKey);
|
|
482
|
+
* ```
|
|
391
483
|
*/
|
|
392
484
|
export const ml_kem512 = /* @__PURE__ */ (() => mk(PARAMS[512]))();
|
|
393
485
|
/**
|
|
@@ -441,4 +533,3 @@ export const __tests = /* @__PURE__ */ (() => Object.freeze({
|
|
|
441
533
|
}
|
|
442
534
|
},
|
|
443
535
|
}))();
|
|
444
|
-
//# sourceMappingURL=ml-kem.js.map
|
package/package.json
CHANGED
|
@@ -1,40 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noble/post-quantum",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Auditable & minimal JS implementation of post-quantum cryptography: FIPS 203, 204, 205, Falcon",
|
|
5
5
|
"files": [
|
|
6
6
|
"*.js",
|
|
7
|
-
"*.js.map",
|
|
8
7
|
"*.d.ts",
|
|
9
|
-
"*.d.ts.map",
|
|
10
8
|
"src"
|
|
11
9
|
],
|
|
12
10
|
"dependencies": {
|
|
13
|
-
"@noble/ciphers": "~2.
|
|
14
|
-
"@noble/curves": "~2.
|
|
15
|
-
"@noble/hashes": "~2.
|
|
11
|
+
"@noble/ciphers": "~2.3.0",
|
|
12
|
+
"@noble/curves": "~2.3.0",
|
|
13
|
+
"@noble/hashes": "~2.3.0"
|
|
16
14
|
},
|
|
17
15
|
"devDependencies": {
|
|
18
|
-
"@paulmillr/jsbt": "0.5
|
|
16
|
+
"@paulmillr/jsbt": "0.6.5",
|
|
19
17
|
"@types/node": "25.3.0",
|
|
20
18
|
"fast-check": "4.2.0",
|
|
21
19
|
"prettier": "3.6.2",
|
|
22
20
|
"typescript": "6.0.2"
|
|
23
21
|
},
|
|
24
22
|
"scripts": {
|
|
25
|
-
"
|
|
23
|
+
"benchmark": "node benchmark/pq.ts",
|
|
24
|
+
"benchmark:size": "npx bismar@0.1 -s",
|
|
26
25
|
"build": "tsc",
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"check:readme": "npx --no @paulmillr/jsbt readme package.json",
|
|
30
|
-
"check:treeshake": "npx --no @paulmillr/jsbt treeshake package.json test/build/out-treeshake",
|
|
31
|
-
"check:jsdoc": "npx --no @paulmillr/jsbt tsdoc package.json",
|
|
32
|
-
"build:clean": "rm *.{js,js.map,d.ts,d.ts.map} 2> /dev/null",
|
|
26
|
+
"check": "jsbt-check",
|
|
27
|
+
"build:clean": "rm *.{js,d.ts} 2> /dev/null",
|
|
33
28
|
"format": "prettier --write 'src/**/*.{js,ts}' 'test/**/*.{js,ts,mjs}'",
|
|
34
29
|
"test": "node test/index.ts",
|
|
35
|
-
"test:bun": "bun test/index.ts",
|
|
36
|
-
"test:deno": "deno --allow-env --allow-read test/index.ts",
|
|
37
|
-
"test:node20": "cd test; npx tsc; node compiled/test/index.js",
|
|
38
30
|
"test:slow": "SLOW_TESTS=1 node test/index.ts"
|
|
39
31
|
},
|
|
40
32
|
"exports": {
|
package/slh-dsa.d.ts
CHANGED
|
@@ -40,12 +40,16 @@ export type SphincsHashOpts = {
|
|
|
40
40
|
export declare const PARAMS: Record<string, SphincsOpts>;
|
|
41
41
|
/** Address byte array of size `ADDR_BYTES`. */
|
|
42
42
|
export type ADRS = Uint8Array;
|
|
43
|
-
/** Hash and tweakable-hash callbacks bound to one SLH-DSA keypair context.
|
|
43
|
+
/** Hash and tweakable-hash callbacks bound to one SLH-DSA keypair context.
|
|
44
|
+
* Buffer-aliasing contract: `PRFaddr`, `thash1` and `thashN` return views into per-context
|
|
45
|
+
* scratch buffers (one per lane), so callers must consume or copy a result before the next
|
|
46
|
+
* call on the same lane. `clean()` wipes the scratch buffers along with the hash states.
|
|
47
|
+
*/
|
|
44
48
|
export type Context = {
|
|
45
49
|
/**
|
|
46
50
|
* Derive a PRF output for one address.
|
|
47
51
|
* @param addr - Address bytes.
|
|
48
|
-
* @returns PRF output bytes.
|
|
52
|
+
* @returns PRF output bytes (scratch view; copy to retain).
|
|
49
53
|
*/
|
|
50
54
|
PRFaddr: (addr: TArg<ADRS>) => TRet<Uint8Array>;
|
|
51
55
|
/**
|
|
@@ -131,6 +135,23 @@ export declare const slh_dsa_shake_256s: TRet<SphincsSigner>;
|
|
|
131
135
|
* SLH-DSA-SHA2-128f: Table 2 row `n=16, h=66, d=22, h'=3, a=6, k=33, lg w=4, m=34`;
|
|
132
136
|
* lengths `publicKey=32`, `secretKey=64`, `signature=17088`, `seed=48`, `signRand=16`.
|
|
133
137
|
* Also exposes `.prehash(...)`.
|
|
138
|
+
* @example
|
|
139
|
+
* Generate deterministic SLH-DSA keys, sign one message, and verify the signature.
|
|
140
|
+
* ```ts
|
|
141
|
+
* import { sha256 } from '@noble/hashes/sha2.js';
|
|
142
|
+
* import { slh_dsa_sha2_128f } from '@noble/post-quantum/slh-dsa.js';
|
|
143
|
+
* const seed = new Uint8Array(slh_dsa_sha2_128f.lengths.seed!);
|
|
144
|
+
* const { secretKey, publicKey } = slh_dsa_sha2_128f.keygen(seed);
|
|
145
|
+
* const msg = new TextEncoder().encode('hello noble');
|
|
146
|
+
* const sig = slh_dsa_sha2_128f.sign(msg, secretKey);
|
|
147
|
+
* const isValid = slh_dsa_sha2_128f.verify(sig, msg, publicKey);
|
|
148
|
+
* const recovered = slh_dsa_sha2_128f.getPublicKey(secretKey);
|
|
149
|
+
* const context = new Uint8Array([1, 2, 3]);
|
|
150
|
+
* const prehash = slh_dsa_sha2_128f.prehash(sha256);
|
|
151
|
+
* const preSig = prehash.sign(msg, secretKey, { context });
|
|
152
|
+
* const preValid = prehash.verify(preSig, msg, publicKey, { context });
|
|
153
|
+
* const internalSig = slh_dsa_sha2_128f.internal.sign(msg, secretKey);
|
|
154
|
+
* ```
|
|
134
155
|
*/
|
|
135
156
|
export declare const slh_dsa_sha2_128f: TRet<SphincsSigner>;
|
|
136
157
|
/**
|
|
@@ -163,4 +184,3 @@ export declare const slh_dsa_sha2_256f: TRet<SphincsSigner>;
|
|
|
163
184
|
* Also exposes `.prehash(...)`.
|
|
164
185
|
*/
|
|
165
186
|
export declare const slh_dsa_sha2_256s: TRet<SphincsSigner>;
|
|
166
|
-
//# sourceMappingURL=slh-dsa.d.ts.map
|