@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/src/hybrid.ts
CHANGED
|
@@ -80,6 +80,8 @@ import { type ECDSA } from '@noble/curves/abstract/weierstrass.js';
|
|
|
80
80
|
import { x25519 } from '@noble/curves/ed25519.js';
|
|
81
81
|
import { p256, p384 } from '@noble/curves/nist.js';
|
|
82
82
|
import {
|
|
83
|
+
abool,
|
|
84
|
+
afunction,
|
|
83
85
|
asciiToBytes,
|
|
84
86
|
bytesToNumberBE,
|
|
85
87
|
bytesToNumberLE,
|
|
@@ -92,6 +94,8 @@ import { sha3_256, shake256 } from '@noble/hashes/sha3.js';
|
|
|
92
94
|
import { abytes, ahash, anumber, type CHash, type CHashXOF } from '@noble/hashes/utils.js';
|
|
93
95
|
import { ml_kem1024, ml_kem768 } from './ml-kem.ts';
|
|
94
96
|
import {
|
|
97
|
+
aobject,
|
|
98
|
+
astring,
|
|
95
99
|
cleanBytes,
|
|
96
100
|
copyBytes,
|
|
97
101
|
randomBytes,
|
|
@@ -108,9 +112,32 @@ import {
|
|
|
108
112
|
type CurveAll = ECDSA | EdDSA | MontgomeryECDH;
|
|
109
113
|
type CurveECDH = ECDSA | MontgomeryECDH;
|
|
110
114
|
type CurveSign = ECDSA | EdDSA;
|
|
115
|
+
const validateKEM = (kem: TArg<KEM>, title: string): TRet<KEM> => {
|
|
116
|
+
const k = aobject<KEM>(kem, title);
|
|
117
|
+
aobject(k.lengths, `${title}.lengths`);
|
|
118
|
+
afunction(k.keygen, `${title}.keygen`);
|
|
119
|
+
afunction(k.getPublicKey, `${title}.getPublicKey`);
|
|
120
|
+
afunction(k.encapsulate, `${title}.encapsulate`);
|
|
121
|
+
afunction(k.decapsulate, `${title}.decapsulate`);
|
|
122
|
+
return k as TRet<KEM>;
|
|
123
|
+
};
|
|
124
|
+
const validateSigner = (signer: TArg<Signer>, title: string): TRet<Signer> => {
|
|
125
|
+
const s = aobject<Signer>(signer, title);
|
|
126
|
+
aobject(s.lengths, `${title}.lengths`);
|
|
127
|
+
afunction(s.keygen, `${title}.keygen`);
|
|
128
|
+
afunction(s.getPublicKey, `${title}.getPublicKey`);
|
|
129
|
+
afunction(s.sign, `${title}.sign`);
|
|
130
|
+
afunction(s.verify, `${title}.verify`);
|
|
131
|
+
return s as TRet<Signer>;
|
|
132
|
+
};
|
|
111
133
|
|
|
112
134
|
// Can re-use if decide to signatures support, on other hand getSecretKey is specific and ugly
|
|
113
135
|
function ecKeygen(curve: CurveAll, allowZeroKey: boolean = false) {
|
|
136
|
+
const c = aobject<CurveAll>(curve, 'curve');
|
|
137
|
+
aobject(c.lengths, 'curve.lengths');
|
|
138
|
+
afunction(c.keygen, 'curve.keygen');
|
|
139
|
+
afunction(c.getPublicKey, 'curve.getPublicKey');
|
|
140
|
+
abool(allowZeroKey, 'allowZeroKey');
|
|
114
141
|
const lengths = curve.lengths;
|
|
115
142
|
let keygen = curve.keygen;
|
|
116
143
|
if (allowZeroKey) {
|
|
@@ -177,6 +204,11 @@ function ecKeygen(curve: CurveAll, allowZeroKey: boolean = false) {
|
|
|
177
204
|
export function ecdhKem(curve: CurveECDH, allowZeroKey: boolean = false): TRet<KEM> {
|
|
178
205
|
const kg = ecKeygen(curve, allowZeroKey);
|
|
179
206
|
if (!curve.getSharedSecret) throw new Error('wrong curve'); // ed25519 doesn't have one!
|
|
207
|
+
// Standalone (not `this.decapsulate`) so encapsulate works even when methods are destructured.
|
|
208
|
+
const decapsulate = (cipherText: TArg<Uint8Array>, secretKey: TArg<Uint8Array>) => {
|
|
209
|
+
const res = curve.getSharedSecret(secretKey, cipherText);
|
|
210
|
+
return (curve.lengths.publicKeyHasPrefix ? res.subarray(1) : res) as TRet<Uint8Array>;
|
|
211
|
+
};
|
|
180
212
|
return {
|
|
181
213
|
lengths: { ...kg.lengths, msg: kg.lengths.seed, cipherText: kg.lengths.publicKey },
|
|
182
214
|
keygen: kg.keygen,
|
|
@@ -190,8 +222,8 @@ export function ecdhKem(curve: CurveECDH, allowZeroKey: boolean = false): TRet<K
|
|
|
190
222
|
const seed = copyBytes(rand);
|
|
191
223
|
let ek: Uint8Array | undefined = undefined;
|
|
192
224
|
try {
|
|
193
|
-
ek =
|
|
194
|
-
const sharedSecret =
|
|
225
|
+
ek = kg.keygen(seed).secretKey;
|
|
226
|
+
const sharedSecret = decapsulate(publicKey, ek);
|
|
195
227
|
const cipherText = curve.getPublicKey(ek) as TRet<Uint8Array>;
|
|
196
228
|
return { sharedSecret, cipherText };
|
|
197
229
|
} finally {
|
|
@@ -201,10 +233,7 @@ export function ecdhKem(curve: CurveECDH, allowZeroKey: boolean = false): TRet<K
|
|
|
201
233
|
if (ek) cleanBytes(ek);
|
|
202
234
|
}
|
|
203
235
|
},
|
|
204
|
-
decapsulate
|
|
205
|
-
const res = curve.getSharedSecret(secretKey, cipherText);
|
|
206
|
-
return (curve.lengths.publicKeyHasPrefix ? res.subarray(1) : res) as TRet<Uint8Array>;
|
|
207
|
-
},
|
|
236
|
+
decapsulate,
|
|
208
237
|
};
|
|
209
238
|
}
|
|
210
239
|
|
|
@@ -359,25 +388,40 @@ function combineKeys(
|
|
|
359
388
|
if (!ok) cleanBytes(secretKey);
|
|
360
389
|
}
|
|
361
390
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
const { publicKey: pk, secretKey } = expandDecapsulationKey(seed);
|
|
391
|
+
// Standalone (not a method) so getPublicKey / destructured usage never depends on `this`.
|
|
392
|
+
const keygen = (seed?: TArg<Uint8Array>) => {
|
|
393
|
+
// Detach the root: the exported secretKey must not alias caller-owned seed bytes, so later
|
|
394
|
+
// caller mutation of the seed cannot silently change the secret key (and vice versa).
|
|
395
|
+
const root = seed === undefined ? randomBytes(realSeedLen!) : copyBytes(seed);
|
|
396
|
+
let res;
|
|
397
|
+
try {
|
|
398
|
+
const { publicKey: pk, secretKey } = expandDecapsulationKey(root);
|
|
371
399
|
try {
|
|
372
|
-
|
|
373
|
-
|
|
400
|
+
res = {
|
|
401
|
+
secretKey: root as TRet<Uint8Array>,
|
|
402
|
+
publicKey: pkCoder.encode(pk) as TRet<Uint8Array>,
|
|
403
|
+
};
|
|
374
404
|
} finally {
|
|
375
|
-
|
|
376
|
-
// The exported secretKey is the caller/root seed itself; child secret keys are internal
|
|
405
|
+
// The exported secretKey is the (detached) root seed; child secret keys are internal
|
|
377
406
|
// expansion outputs that are cleaned whether encoding succeeds or throws.
|
|
378
|
-
cleanBytes(secretKey);
|
|
407
|
+
cleanBytes(pk, secretKey);
|
|
379
408
|
}
|
|
409
|
+
return res;
|
|
410
|
+
} finally {
|
|
411
|
+
if (!res) cleanBytes(root);
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
return {
|
|
415
|
+
info: { lengths: { seed: realSeedLen, publicKey: pkCoder.bytesLen, secretKey: realSeedLen } },
|
|
416
|
+
// Composite secret keys are root seeds, so public-key derivation reruns key expansion from
|
|
417
|
+
// that seed instead of decoding a packed child-secret-key structure.
|
|
418
|
+
getPublicKey: (secretKey: TArg<Uint8Array>) => {
|
|
419
|
+
const keys = keygen(secretKey);
|
|
420
|
+
// keygen detaches its exported root; getPublicKey discards that half of the result.
|
|
421
|
+
cleanBytes(keys.secretKey);
|
|
422
|
+
return keys.publicKey as TRet<Uint8Array>;
|
|
380
423
|
},
|
|
424
|
+
keygen,
|
|
381
425
|
expandDecapsulationKey,
|
|
382
426
|
realSeedLen,
|
|
383
427
|
};
|
|
@@ -416,14 +460,21 @@ export function combineKEMS(
|
|
|
416
460
|
combiner: TArg<Combiner>,
|
|
417
461
|
...kems: TArg<KEM[]>
|
|
418
462
|
): TRet<KEM> {
|
|
463
|
+
if (realSeedLen !== undefined) anumber(realSeedLen, 'realSeedLen');
|
|
464
|
+
if (realMsgLen !== undefined) anumber(realMsgLen, 'realMsgLen');
|
|
465
|
+
if (typeof expandSeed !== 'function')
|
|
466
|
+
throw new TypeError('"expandSeed" expected function, got type=' + typeof expandSeed);
|
|
467
|
+
if (typeof combiner !== 'function')
|
|
468
|
+
throw new TypeError('"combiner" expected function, got type=' + typeof combiner);
|
|
419
469
|
const rawCombiner = combiner as Combiner;
|
|
420
470
|
const rawKems = kems as KEM[];
|
|
471
|
+
for (let i = 0; i < rawKems.length; i++) validateKEM(rawKems[i], `kems[${i}]`);
|
|
421
472
|
const keys = combineKeys(realSeedLen, expandSeed, ...rawKems);
|
|
422
473
|
const ctCoder = splitLengths(rawKems, 'cipherText');
|
|
423
474
|
const pkCoder = splitLengths(rawKems, 'publicKey');
|
|
424
475
|
const msgCoder = splitLengths(rawKems, 'msg');
|
|
425
476
|
if (realMsgLen === undefined) realMsgLen = msgCoder.bytesLen;
|
|
426
|
-
anumber(realMsgLen);
|
|
477
|
+
anumber(realMsgLen, 'realMsgLen');
|
|
427
478
|
const lengths = Object.freeze({
|
|
428
479
|
...keys.info.lengths,
|
|
429
480
|
msg: realMsgLen,
|
|
@@ -491,7 +542,11 @@ export function combineKEMS(
|
|
|
491
542
|
* import { combineSigners, expandSeedXof } from '@noble/post-quantum/hybrid.js';
|
|
492
543
|
* import { ml_dsa44 } from '@noble/post-quantum/ml-dsa.js';
|
|
493
544
|
* const hybrid = combineSigners(32, expandSeedXof(shake256), ml_dsa44, ml_dsa44);
|
|
494
|
-
* const
|
|
545
|
+
* const seed = new Uint8Array(hybrid.lengths.seed!).fill(1);
|
|
546
|
+
* const { secretKey, publicKey } = hybrid.keygen(seed);
|
|
547
|
+
* const msg = new TextEncoder().encode('hello noble');
|
|
548
|
+
* const sig = hybrid.sign(msg, secretKey);
|
|
549
|
+
* const isValid = hybrid.verify(sig, msg, publicKey);
|
|
495
550
|
* ```
|
|
496
551
|
*/
|
|
497
552
|
export function combineSigners(
|
|
@@ -499,7 +554,11 @@ export function combineSigners(
|
|
|
499
554
|
expandSeed: TArg<ExpandSeed>,
|
|
500
555
|
...signers: TArg<Signer[]>
|
|
501
556
|
): TRet<Signer> {
|
|
557
|
+
if (realSeedLen !== undefined) anumber(realSeedLen, 'realSeedLen');
|
|
558
|
+
if (typeof expandSeed !== 'function')
|
|
559
|
+
throw new TypeError('"expandSeed" expected function, got type=' + typeof expandSeed);
|
|
502
560
|
const rawSigners = signers as Signer[];
|
|
561
|
+
for (let i = 0; i < rawSigners.length; i++) validateSigner(rawSigners[i], `signers[${i}]`);
|
|
503
562
|
const keys = combineKeys(realSeedLen, expandSeed, ...rawSigners);
|
|
504
563
|
const sigCoder = splitLengths(rawSigners, 'signature');
|
|
505
564
|
const pkCoder = splitLengths(rawSigners, 'publicKey');
|
|
@@ -531,8 +590,8 @@ export function combineSigners(
|
|
|
531
590
|
}
|
|
532
591
|
},
|
|
533
592
|
/** Verify one combined signature.
|
|
534
|
-
*
|
|
535
|
-
*
|
|
593
|
+
* Wrong-length aggregate signatures return `false` (matching ml-dsa / slh-dsa behavior), as
|
|
594
|
+
* does any failing child verify. Throws on unsupported generic opts or malformed publicKey.
|
|
536
595
|
*/
|
|
537
596
|
verify: (signature, message, publicKey, opts = {}) => {
|
|
538
597
|
validateVerOpts(opts);
|
|
@@ -540,7 +599,13 @@ export function combineSigners(
|
|
|
540
599
|
throw new Error(
|
|
541
600
|
'combineSigners does not support context; use the underlying signer directly'
|
|
542
601
|
);
|
|
602
|
+
// Malformed signature *length* is a verification failure, not a thrown type error —
|
|
603
|
+
// consistent with ml-dsa / slh-dsa. Must run before sigCoder.decode, which throws.
|
|
604
|
+
// Preserve TypeError for non-byte API arguments before treating byte lengths as invalid.
|
|
605
|
+
abytes(signature, undefined, 'signature');
|
|
606
|
+
// A signature failure must not hide malformed aggregate public-key bytes.
|
|
543
607
|
const pks = pkCoder.decode(publicKey);
|
|
608
|
+
if (signature.length !== sigCoder.bytesLen) return false;
|
|
544
609
|
const sigs = sigCoder.decode(signature);
|
|
545
610
|
for (let i = 0; i < rawSigners.length; i++) {
|
|
546
611
|
if (!rawSigners[i].verify(sigs[i], message, pks[i])) return false;
|
|
@@ -581,7 +646,14 @@ export function QSF(
|
|
|
581
646
|
xof: TArg<XOF>,
|
|
582
647
|
kdf: CHash
|
|
583
648
|
): TRet<KEM> {
|
|
649
|
+
astring(label, 'label');
|
|
650
|
+
validateKEM(pqc, 'pqc');
|
|
651
|
+
validateKEM(curveKEM, 'curveKEM');
|
|
652
|
+
if (typeof xof !== 'function' || typeof (xof as any).create !== 'function')
|
|
653
|
+
throw new TypeError('"xof" expected hash function, got type=' + typeof xof);
|
|
584
654
|
ahash(xof);
|
|
655
|
+
if (typeof kdf !== 'function' || typeof (kdf as any).create !== 'function')
|
|
656
|
+
throw new TypeError('"kdf" expected hash function, got type=' + typeof kdf);
|
|
585
657
|
ahash(kdf);
|
|
586
658
|
return combineKEMS(
|
|
587
659
|
32,
|
|
@@ -646,7 +718,14 @@ export function createKitchenSink(
|
|
|
646
718
|
xof: TArg<XOF>,
|
|
647
719
|
hash: CHash
|
|
648
720
|
): TRet<KEM> {
|
|
721
|
+
astring(label, 'label');
|
|
722
|
+
validateKEM(pqc, 'pqc');
|
|
723
|
+
validateKEM(curveKEM, 'curveKEM');
|
|
724
|
+
if (typeof xof !== 'function' || typeof (xof as any).create !== 'function')
|
|
725
|
+
throw new TypeError('"xof" expected hash function, got type=' + typeof xof);
|
|
649
726
|
ahash(xof);
|
|
727
|
+
if (typeof hash !== 'function' || typeof (hash as any).create !== 'function')
|
|
728
|
+
throw new TypeError('"hash" expected hash function, got type=' + typeof hash);
|
|
650
729
|
ahash(hash);
|
|
651
730
|
return combineKEMS(
|
|
652
731
|
32,
|
|
@@ -734,6 +813,11 @@ function nistCurveKem(curve: ECDSA, scalarLen: number, elemLen: number, nseed: n
|
|
|
734
813
|
}>;
|
|
735
814
|
}
|
|
736
815
|
|
|
816
|
+
// Standalone (not `this.decapsulate`) so encapsulate works even when methods are destructured.
|
|
817
|
+
const decapsulate = (cipherText: TArg<Uint8Array>, secretKey: TArg<Uint8Array>) => {
|
|
818
|
+
const full = curve.getSharedSecret(secretKey, cipherText);
|
|
819
|
+
return full.subarray(1) as TRet<Uint8Array>;
|
|
820
|
+
};
|
|
737
821
|
return {
|
|
738
822
|
lengths: {
|
|
739
823
|
secretKey: scalarLen,
|
|
@@ -754,7 +838,7 @@ function nistCurveKem(curve: ECDSA, scalarLen: number, elemLen: number, nseed: n
|
|
|
754
838
|
let ek: Uint8Array | undefined = undefined;
|
|
755
839
|
try {
|
|
756
840
|
ek = rejectionSampling(rand).secretKey;
|
|
757
|
-
const sharedSecret =
|
|
841
|
+
const sharedSecret = decapsulate(publicKey, ek);
|
|
758
842
|
const cipherText = curve.getPublicKey(ek, false) as TRet<Uint8Array>;
|
|
759
843
|
return { sharedSecret, cipherText };
|
|
760
844
|
} finally {
|
|
@@ -763,10 +847,7 @@ function nistCurveKem(curve: ECDSA, scalarLen: number, elemLen: number, nseed: n
|
|
|
763
847
|
if (ek) cleanBytes(ek);
|
|
764
848
|
}
|
|
765
849
|
},
|
|
766
|
-
decapsulate
|
|
767
|
-
const full = curve.getSharedSecret(secretKey, cipherText);
|
|
768
|
-
return full.subarray(1) as TRet<Uint8Array>;
|
|
769
|
-
},
|
|
850
|
+
decapsulate,
|
|
770
851
|
};
|
|
771
852
|
}
|
|
772
853
|
|
|
@@ -795,10 +876,11 @@ function concreteHybridKem(
|
|
|
795
876
|
32,
|
|
796
877
|
(seed: TArg<Uint8Array>): TRet<Uint8Array> => {
|
|
797
878
|
abytes(seed, 32);
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
879
|
+
// One SHAKE256 stream split by the seed coder as mlkemSeed (64) || curveSeed (nseed).
|
|
880
|
+
// Returned directly: the previous concatBytes of two adjacent subarrays produced an
|
|
881
|
+
// identical copy while leaving this original buffer unwiped; expandDecapsulationKey
|
|
882
|
+
// wipes the returned buffer after the child seeds are copied out.
|
|
883
|
+
return shake256(seed, { dkLen: totalSeedLen }) as TRet<Uint8Array>;
|
|
802
884
|
},
|
|
803
885
|
(pk: TArg<Uint8Array[]>, ct: TArg<Uint8Array[]>, ss: TArg<Uint8Array[]>) =>
|
|
804
886
|
sha3_256(concatBytes(ss[0], ss[1], ct[1], pk[1], asciiToBytes(label))),
|
|
@@ -814,20 +896,3 @@ export const ml_kem768_p256: TRet<KEM> = /* @__PURE__ */ (() =>
|
|
|
814
896
|
/** P-384 + ML-KEM-1024 hybrid preset. */
|
|
815
897
|
export const ml_kem1024_p384: TRet<KEM> = /* @__PURE__ */ (() =>
|
|
816
898
|
concreteHybridKem('MLKEM1024-P384', ml_kem1024, p384, 48))();
|
|
817
|
-
|
|
818
|
-
// Legacy aliases
|
|
819
|
-
/** Legacy alias for `ml_kem768_x25519`. */
|
|
820
|
-
export const XWing: TRet<KEM> = /* @__PURE__ */ (() => ml_kem768_x25519)();
|
|
821
|
-
/** Legacy alias for `ml_kem768_x25519`. */
|
|
822
|
-
export const MLKEM768X25519: TRet<KEM> = /* @__PURE__ */ (() => ml_kem768_x25519)();
|
|
823
|
-
/** Legacy alias for `ml_kem768_p256`. */
|
|
824
|
-
export const MLKEM768P256: TRet<KEM> = /* @__PURE__ */ (() => ml_kem768_p256)();
|
|
825
|
-
/** Legacy alias for `ml_kem1024_p384`. */
|
|
826
|
-
export const MLKEM1024P384: TRet<KEM> = /* @__PURE__ */ (() => ml_kem1024_p384)();
|
|
827
|
-
/** Legacy alias for `QSF_ml_kem768_p256`. */
|
|
828
|
-
export const QSFMLKEM768P256: TRet<KEM> = /* @__PURE__ */ (() => QSF_ml_kem768_p256)();
|
|
829
|
-
/** Legacy alias for `QSF_ml_kem1024_p384`. */
|
|
830
|
-
export const QSFMLKEM1024P384: TRet<KEM> = /* @__PURE__ */ (() => QSF_ml_kem1024_p384)();
|
|
831
|
-
/** Legacy alias for `KitchenSink_ml_kem768_x25519`. */
|
|
832
|
-
export const KitchenSinkMLKEM768X25519: TRet<KEM> = /* @__PURE__ */ (() =>
|
|
833
|
-
KitchenSink_ml_kem768_x25519)();
|
package/src/ml-dsa.ts
CHANGED
|
@@ -64,7 +64,16 @@ export type DSAInternal = CryptoKeys & {
|
|
|
64
64
|
) => boolean;
|
|
65
65
|
};
|
|
66
66
|
/** Public ML-DSA signer surface. */
|
|
67
|
-
export type DSA = Signer & {
|
|
67
|
+
export type DSA = Signer & {
|
|
68
|
+
internal: TRet<DSAInternal>;
|
|
69
|
+
securityLevel: number;
|
|
70
|
+
/**
|
|
71
|
+
* HashML-DSA (FIPS 204 §5.4) variant which signs a pre-hashed message.
|
|
72
|
+
* @param hash - Approved hash, checked against the parameter set security level.
|
|
73
|
+
* @returns Signer which pre-hashes `msg` before formatting `M'`.
|
|
74
|
+
*/
|
|
75
|
+
prehash: (hash: TArg<CHash>) => TRet<Signer>;
|
|
76
|
+
};
|
|
68
77
|
|
|
69
78
|
// Constants
|
|
70
79
|
// FIPS 204 fixes ML-DSA over R = Z[X]/(X^256 + 1), so every polynomial has 256 coefficients.
|
|
@@ -154,6 +163,8 @@ const polyCoder = (d: number, compress: IdNum = id, verify: IdNum = id) =>
|
|
|
154
163
|
});
|
|
155
164
|
|
|
156
165
|
// Mutates `a` in place; callers must pass same-length polynomials.
|
|
166
|
+
// NOTE: conditional-reduction variants (as in ml-kem) were measured performance-neutral here —
|
|
167
|
+
// int32 `%` with 23-bit Q is already cheap — so the simpler mod() form is kept for audit.
|
|
157
168
|
const polyAdd = (a_: TArg<Poly>, b_: TArg<Poly>): TRet<Poly> => {
|
|
158
169
|
const a = a_ as Poly;
|
|
159
170
|
const b = b_ as Poly;
|
|
@@ -275,13 +286,16 @@ function getDilithium(opts_: TArg<DilithiumOpts>): TRet<DSA> {
|
|
|
275
286
|
return res0;
|
|
276
287
|
};
|
|
277
288
|
|
|
289
|
+
// m = (q-1)/(2γ2): 44 for ML-DSA-44, 16 for 65/87. Hoisted out of UseHint, which runs
|
|
290
|
+
// per coefficient during verification.
|
|
291
|
+
const HINT_M = Math.floor((Q - 1) / (2 * GAMMA2));
|
|
278
292
|
const UseHint = (h: number, r: number) => {
|
|
279
293
|
// Returns the high bits of r adjusted according to hint h
|
|
280
|
-
const m = Math.floor((Q - 1) / (2 * GAMMA2));
|
|
281
294
|
const { r1, r0 } = decompose(r);
|
|
282
295
|
// 3: if h = 1 and r0 > 0 return (r1 + 1) mod m
|
|
283
296
|
// 4: if h = 1 and r0 ≤ 0 return (r1 − 1) mod m
|
|
284
|
-
if (h === 1)
|
|
297
|
+
if (h === 1)
|
|
298
|
+
return r0 > 0 ? crystals.mod(r1 + 1, HINT_M) | 0 : crystals.mod(r1 - 1, HINT_M) | 0;
|
|
285
299
|
return r1 | 0;
|
|
286
300
|
};
|
|
287
301
|
const Power2Round = (r: number) => {
|
|
@@ -527,11 +541,32 @@ function getDilithium(opts_: TArg<DilithiumOpts>): TRet<DSA> {
|
|
|
527
541
|
): TRet<Uint8Array> => {
|
|
528
542
|
validateSigOpts(opts);
|
|
529
543
|
validateInternalOpts(opts);
|
|
530
|
-
|
|
544
|
+
const { extraEntropy: random, externalMu = false } = opts;
|
|
545
|
+
// FIPS 204 external-mu mode expects the 64-byte message representative µ = H(tr || M).
|
|
546
|
+
if (externalMu) abytes(msg, CRH_BYTES, 'mu');
|
|
547
|
+
// Prepare entropy before touching decoded secrets: randomBytes() may throw, and an RNG
|
|
548
|
+
// failure must not leave expanded secret-polynomial copies behind.
|
|
549
|
+
const ownRnd = random === false || random === undefined;
|
|
550
|
+
const rnd =
|
|
551
|
+
random === false
|
|
552
|
+
? new Uint8Array(32)
|
|
553
|
+
: random === undefined
|
|
554
|
+
? randomBytes(signRandBytes)
|
|
555
|
+
: (random as Uint8Array);
|
|
556
|
+
abytes(rnd, 32, 'extraEntropy');
|
|
531
557
|
// This part can be pre-cached per secretKey, but there is only minor performance improvement,
|
|
532
558
|
// since we re-use a lot of variables to computation.
|
|
533
559
|
// (ρ, K,tr, s1, s2, t0) ← skDecode(sk)
|
|
534
|
-
const
|
|
560
|
+
const decoded = (() => {
|
|
561
|
+
try {
|
|
562
|
+
return secretCoder.decode(secretKey);
|
|
563
|
+
} catch (error) {
|
|
564
|
+
// A malformed key must not strand entropy owned by the library.
|
|
565
|
+
if (ownRnd) cleanBytes(rnd);
|
|
566
|
+
throw error;
|
|
567
|
+
}
|
|
568
|
+
})();
|
|
569
|
+
const [rho, _K, tr, s1, s2, t0] = decoded;
|
|
535
570
|
// Cache matrix to avoid re-compute later
|
|
536
571
|
const A: Poly[][] = []; // A ← ExpandA(ρ)
|
|
537
572
|
const xof = XOF128(rho);
|
|
@@ -553,21 +588,14 @@ function getDilithium(opts_: TArg<DilithiumOpts>): TRet<DSA> {
|
|
|
553
588
|
// ▷ Compute message representative µ
|
|
554
589
|
shake256.create({ dkLen: CRH_BYTES }).update(tr).update(msg).digest();
|
|
555
590
|
|
|
556
|
-
// Compute private random seed
|
|
557
|
-
const rnd =
|
|
558
|
-
random === false
|
|
559
|
-
? new Uint8Array(32)
|
|
560
|
-
: random === undefined
|
|
561
|
-
? randomBytes(signRandBytes)
|
|
562
|
-
: random;
|
|
563
|
-
abytes(rnd, 32, 'extraEntropy');
|
|
564
591
|
const rhoprime = shake256
|
|
565
592
|
.create({ dkLen: CRH_BYTES })
|
|
566
593
|
.update(_K)
|
|
567
594
|
.update(rnd)
|
|
568
595
|
.update(mu)
|
|
569
596
|
.digest(); // ρ′← H(K||rnd||µ, 512)
|
|
570
|
-
|
|
597
|
+
// Only wipe entropy we generated; caller-provided extraEntropy stays caller-owned.
|
|
598
|
+
if (ownRnd) cleanBytes(rnd);
|
|
571
599
|
abytes(rhoprime, CRH_BYTES);
|
|
572
600
|
const x256 = XOF256(rhoprime, ZCoder.bytesLen);
|
|
573
601
|
// Rejection sampling loop
|
|
@@ -638,6 +666,8 @@ function getDilithium(opts_: TArg<DilithiumOpts>): TRet<DSA> {
|
|
|
638
666
|
) => {
|
|
639
667
|
validateInternalOpts(opts);
|
|
640
668
|
const { externalMu = false } = opts;
|
|
669
|
+
// FIPS 204 external-mu mode expects the 64-byte message representative µ = H(tr || M).
|
|
670
|
+
if (externalMu) abytes(msg, CRH_BYTES, 'mu');
|
|
641
671
|
// ML-DSA.Verify(pk, M, σ): Verifes a signature σ for a message M.
|
|
642
672
|
const [rho, t1] = publicCoder.decode(publicKey); // (ρ, t1) ← pkDecode(pk)
|
|
643
673
|
const tr = shake256(publicKey, { dkLen: TR_BYTES }); // 6: tr ← H(BytesToBits(pk), 512)
|
|
@@ -712,10 +742,12 @@ function getDilithium(opts_: TArg<DilithiumOpts>): TRet<DSA> {
|
|
|
712
742
|
opts: TArg<VerOpts> = {}
|
|
713
743
|
) => {
|
|
714
744
|
validateVerOpts(opts);
|
|
745
|
+
abytes(sig, undefined, 'signature');
|
|
715
746
|
return internal.verify(sig, getMessage(msg, opts.context), publicKey);
|
|
716
747
|
},
|
|
717
|
-
prehash: (hash: CHash) => {
|
|
718
|
-
checkHash(hash, securityLevel);
|
|
748
|
+
prehash: (hash: TArg<CHash>): TRet<Signer> => {
|
|
749
|
+
checkHash(hash as CHash, securityLevel);
|
|
750
|
+
const rawHash = hash as CHash;
|
|
719
751
|
return Object.freeze({
|
|
720
752
|
info: Object.freeze({ type: 'hashml-dsa' }),
|
|
721
753
|
securityLevel: securityLevel,
|
|
@@ -728,7 +760,7 @@ function getDilithium(opts_: TArg<DilithiumOpts>): TRet<DSA> {
|
|
|
728
760
|
opts: TArg<SigOpts> = {}
|
|
729
761
|
): TRet<Uint8Array> => {
|
|
730
762
|
validateSigOpts(opts);
|
|
731
|
-
const M = getMessagePrehash(
|
|
763
|
+
const M = getMessagePrehash(rawHash, msg, opts.context);
|
|
732
764
|
const res = internal.sign(M, secretKey, opts);
|
|
733
765
|
cleanBytes(M);
|
|
734
766
|
return res as TRet<Uint8Array>;
|
|
@@ -740,14 +772,34 @@ function getDilithium(opts_: TArg<DilithiumOpts>): TRet<DSA> {
|
|
|
740
772
|
opts: TArg<VerOpts> = {}
|
|
741
773
|
) => {
|
|
742
774
|
validateVerOpts(opts);
|
|
743
|
-
|
|
775
|
+
abytes(sig, undefined, 'signature');
|
|
776
|
+
return internal.verify(sig, getMessagePrehash(rawHash, msg, opts.context), publicKey);
|
|
744
777
|
},
|
|
745
778
|
});
|
|
746
779
|
},
|
|
747
780
|
});
|
|
748
781
|
}
|
|
749
782
|
|
|
750
|
-
/**
|
|
783
|
+
/**
|
|
784
|
+
* ML-DSA-44 for 128-bit security level. Not recommended after 2030, as per ASD.
|
|
785
|
+
* @example
|
|
786
|
+
* Generate deterministic ML-DSA-44 keys, sign one message, and verify the signature.
|
|
787
|
+
* ```ts
|
|
788
|
+
* import { sha256 } from '@noble/hashes/sha2.js';
|
|
789
|
+
* import { ml_dsa44 } from '@noble/post-quantum/ml-dsa.js';
|
|
790
|
+
* const seed = new Uint8Array(ml_dsa44.lengths.seed!);
|
|
791
|
+
* const { secretKey, publicKey } = ml_dsa44.keygen(seed);
|
|
792
|
+
* const msg = new TextEncoder().encode('hello noble');
|
|
793
|
+
* const sig = ml_dsa44.sign(msg, secretKey);
|
|
794
|
+
* const isValid = ml_dsa44.verify(sig, msg, publicKey);
|
|
795
|
+
* const recovered = ml_dsa44.getPublicKey(secretKey);
|
|
796
|
+
* const context = new Uint8Array([1, 2, 3]);
|
|
797
|
+
* const prehash = ml_dsa44.prehash(sha256);
|
|
798
|
+
* const preSig = prehash.sign(msg, secretKey, { context });
|
|
799
|
+
* const preValid = prehash.verify(preSig, msg, publicKey, { context });
|
|
800
|
+
* const internalSig = ml_dsa44.internal.sign(msg, secretKey);
|
|
801
|
+
* ```
|
|
802
|
+
*/
|
|
751
803
|
export const ml_dsa44: TRet<DSA> = /* @__PURE__ */ (() =>
|
|
752
804
|
getDilithium({
|
|
753
805
|
...PARAMS[2],
|