@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/falcon.js
CHANGED
|
@@ -403,6 +403,17 @@ const Q = 12289; // 12 * 1024 + 1
|
|
|
403
403
|
// Falcon's midpoint floor(q/2); the only live use is the mirrored G-reconstruction reduction below.
|
|
404
404
|
const Qhalf = Q >> 1;
|
|
405
405
|
const QBig = BigInt(Q);
|
|
406
|
+
const _0n = /* @__PURE__ */ BigInt(0);
|
|
407
|
+
const _1n = /* @__PURE__ */ BigInt(1);
|
|
408
|
+
const _10n = /* @__PURE__ */ BigInt(10);
|
|
409
|
+
const _25n = /* @__PURE__ */ BigInt(25);
|
|
410
|
+
const _31n = /* @__PURE__ */ BigInt(31);
|
|
411
|
+
const _32n = /* @__PURE__ */ BigInt(32);
|
|
412
|
+
const _63n = /* @__PURE__ */ BigInt(63);
|
|
413
|
+
const _64n = /* @__PURE__ */ BigInt(64);
|
|
414
|
+
// Low 32 bits and low 63 bits of a bigint, used by the chacha20 counter and gaussian sampler.
|
|
415
|
+
const MASK_32n = /* @__PURE__ */ BigInt('0xffffffff');
|
|
416
|
+
const MASK_63n = /* @__PURE__ */ BigInt('0x7fffffffffffffff');
|
|
406
417
|
//const R = 4091; // 2^16 mod q
|
|
407
418
|
// This 16-bit Montgomery kernel uses R = 2^16, so mul(x, R2) converts x into Montgomery form.
|
|
408
419
|
const R2 = 10952; // 2^32 mod q
|
|
@@ -439,35 +450,35 @@ const BITLENGTH = [
|
|
|
439
450
|
// Smaller Falcon dimensions reuse the N = 1024, q = 12289 table by summing 2^(10-logn) draws.
|
|
440
451
|
// The trailing 0 sentinel guarantees gaussSingle()
|
|
441
452
|
// always selects a tail bucket when x = 0 is missed.
|
|
442
|
-
const gauss_1024_12289 = [
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
];
|
|
453
|
+
const gauss_1024_12289 = /* @__PURE__ */ [
|
|
454
|
+
'1283868770400643928',
|
|
455
|
+
'6416574995475331444',
|
|
456
|
+
'4078260278032692663',
|
|
457
|
+
'2353523259288686585',
|
|
458
|
+
'1227179971273316331',
|
|
459
|
+
'575931623374121527',
|
|
460
|
+
'242543240509105209',
|
|
461
|
+
'91437049221049666',
|
|
462
|
+
'30799446349977173',
|
|
463
|
+
'9255276791179340',
|
|
464
|
+
'2478152334826140',
|
|
465
|
+
'590642893610164',
|
|
466
|
+
'125206034929641',
|
|
467
|
+
'23590435911403',
|
|
468
|
+
'3948334035941',
|
|
469
|
+
'586753615614',
|
|
470
|
+
'77391054539',
|
|
471
|
+
'9056793210',
|
|
472
|
+
'940121950',
|
|
473
|
+
'86539696',
|
|
474
|
+
'7062824',
|
|
475
|
+
'510971',
|
|
476
|
+
'32764',
|
|
477
|
+
'1862',
|
|
478
|
+
'94',
|
|
479
|
+
'4',
|
|
480
|
+
'0',
|
|
481
|
+
].map(BigInt);
|
|
471
482
|
// Exact binary64 1/sigma payloads from round-3 fpr.h. Nearby decimal spellings round 1 ULP low in
|
|
472
483
|
// JS, so keep these as decoded bit patterns and recheck the raw payloads after edits.
|
|
473
484
|
const INV_SIGMA = /* @__PURE__ */ Object.freeze([
|
|
@@ -1147,9 +1158,9 @@ function genFalcon(opts) {
|
|
|
1147
1158
|
let val = 0;
|
|
1148
1159
|
for (let i = 0; i < g; i++) {
|
|
1149
1160
|
const r128 = bytesToNumberLE(this.shake.xof(16));
|
|
1150
|
-
const r1 = r128 &
|
|
1151
|
-
const r2 = (r128 >>
|
|
1152
|
-
const sign = Number((r128 >>
|
|
1161
|
+
const r1 = r128 & MASK_63n;
|
|
1162
|
+
const r2 = (r128 >> _64n) & MASK_63n;
|
|
1163
|
+
const sign = Number((r128 >> _63n) & _1n);
|
|
1153
1164
|
let f = r1 < gauss_1024_12289[0] ? 1 : 0;
|
|
1154
1165
|
let v = 0;
|
|
1155
1166
|
for (let k = 1; k < gauss_1024_12289.length; k++) {
|
|
@@ -1186,7 +1197,7 @@ function genFalcon(opts) {
|
|
|
1186
1197
|
const n = 1 << logn;
|
|
1187
1198
|
const d = new Array(n >> 1);
|
|
1188
1199
|
for (let k = 0; k < n; k += 2) {
|
|
1189
|
-
let s =
|
|
1200
|
+
let s = _0n;
|
|
1190
1201
|
for (let i = 0; i <= k; i += 2)
|
|
1191
1202
|
s += a[i] * a[k - i];
|
|
1192
1203
|
for (let i = k + 2; i < n; i += 2)
|
|
@@ -1194,7 +1205,7 @@ function genFalcon(opts) {
|
|
|
1194
1205
|
d[k >>> 1] = s;
|
|
1195
1206
|
}
|
|
1196
1207
|
for (let k = 0; k < n; k += 2) {
|
|
1197
|
-
let s =
|
|
1208
|
+
let s = _0n;
|
|
1198
1209
|
for (let i = 1; i < k; i += 2)
|
|
1199
1210
|
s += a[i] * a[k - i];
|
|
1200
1211
|
for (let i = k + 1; i < n; i += 2)
|
|
@@ -1206,7 +1217,7 @@ function genFalcon(opts) {
|
|
|
1206
1217
|
mulConjD(logn, d, a, b) {
|
|
1207
1218
|
const n = 1 << logn;
|
|
1208
1219
|
for (let k = 0; k < n; k++) {
|
|
1209
|
-
let s =
|
|
1220
|
+
let s = _0n;
|
|
1210
1221
|
for (let i = 0; i <= k; i += 2)
|
|
1211
1222
|
s += b[i >>> 1] * a[k - i];
|
|
1212
1223
|
for (let i = k + 2 - (k & 1); i < n; i += 2)
|
|
@@ -1221,7 +1232,7 @@ function genFalcon(opts) {
|
|
|
1221
1232
|
subMul(logn, a, b, c, e) {
|
|
1222
1233
|
const n = 1 << logn;
|
|
1223
1234
|
for (let k = 0; k < n; k++) {
|
|
1224
|
-
let s =
|
|
1235
|
+
let s = _0n;
|
|
1225
1236
|
for (let i = 0; i <= k; i++)
|
|
1226
1237
|
s += b[i] * c[k - i];
|
|
1227
1238
|
for (let i = k + 1; i < n; i++)
|
|
@@ -1265,7 +1276,7 @@ function genFalcon(opts) {
|
|
|
1265
1276
|
const Gx = new Float64Array(n);
|
|
1266
1277
|
const k = new Array(n);
|
|
1267
1278
|
while (true) {
|
|
1268
|
-
let scaleFG =
|
|
1279
|
+
let scaleFG = _31n * (FGlen - _10n);
|
|
1269
1280
|
for (let i = 0; i < n; i++) {
|
|
1270
1281
|
Fx[i] = Number(F[i] >> scaleFG);
|
|
1271
1282
|
Gx[i] = Number(G[i] >> scaleFG);
|
|
@@ -1284,16 +1295,16 @@ function genFalcon(opts) {
|
|
|
1284
1295
|
}
|
|
1285
1296
|
F = this.subMul(logn, F, f, k, scaleK); // 3: F ← F - kf
|
|
1286
1297
|
G = this.subMul(logn, G, g, k, scaleK); // 4: G ← G - kg
|
|
1287
|
-
const maxfgNew = scaleK + BigInt(Math.round(fgMaxBits)) +
|
|
1298
|
+
const maxfgNew = scaleK + BigInt(Math.round(fgMaxBits)) + _10n;
|
|
1288
1299
|
if (maxfgNew < maxFGBits)
|
|
1289
1300
|
maxFGBits = maxfgNew;
|
|
1290
|
-
if (FGlen >
|
|
1301
|
+
if (FGlen > _1n && FGlen * _31n >= maxFGBits + _31n)
|
|
1291
1302
|
FGlen--;
|
|
1292
|
-
if (scaleK <=
|
|
1303
|
+
if (scaleK <= _0n)
|
|
1293
1304
|
break;
|
|
1294
|
-
scaleK -=
|
|
1295
|
-
if (scaleK <
|
|
1296
|
-
scaleK =
|
|
1305
|
+
scaleK -= _25n;
|
|
1306
|
+
if (scaleK < _0n)
|
|
1307
|
+
scaleK = _0n;
|
|
1297
1308
|
}
|
|
1298
1309
|
return true;
|
|
1299
1310
|
}
|
|
@@ -1321,11 +1332,11 @@ function genFalcon(opts) {
|
|
|
1321
1332
|
const xf = f[0];
|
|
1322
1333
|
const xg = g[0];
|
|
1323
1334
|
// We can rely on 'invert' to throw if they are not coprime.
|
|
1324
|
-
if (xf <=
|
|
1335
|
+
if (xf <= _0n || xg <= _0n)
|
|
1325
1336
|
return false;
|
|
1326
1337
|
try {
|
|
1327
1338
|
const u1 = invert(xf, xg); // if gcd(f, g) ≠ 1 then
|
|
1328
|
-
const v1 = (
|
|
1339
|
+
const v1 = (_1n - u1 * xf) / xg;
|
|
1329
1340
|
F[0] = -v1 * QBig; // 5: (F, G) ← (vq, uq)
|
|
1330
1341
|
G[0] = u1 * QBig;
|
|
1331
1342
|
return true;
|
|
@@ -1607,7 +1618,14 @@ function genFalcon(opts) {
|
|
|
1607
1618
|
return headerCoder(0x30 + logn, splitCoder('falcon.signature', NONCELEN, sigLen, msg.length)).encode([nonce, pad(sigLen).encode(s2), msg]);
|
|
1608
1619
|
},
|
|
1609
1620
|
decode(data) {
|
|
1621
|
+
// Keep API misuse on the coder's TypeError path before reading the container length.
|
|
1622
|
+
abytes(data, undefined, 'signature');
|
|
1623
|
+
// The compressed-signature field is fixed-width here; only the message is variable. A
|
|
1624
|
+
// container shorter than the fixed part would make the s2 field borrow bytes from nowhere
|
|
1625
|
+
// and let a truncated encoding open to the same message.
|
|
1610
1626
|
const msgLen = data.length - NONCELEN - sigLen - 1;
|
|
1627
|
+
if (msgLen < 0)
|
|
1628
|
+
throw new Error('signature coder: wrong length');
|
|
1611
1629
|
const [nonce, s2, msg] = headerCoder(0x30 + logn, splitCoder('falcon.signature', NONCELEN, sigLen, msgLen)).decode(data);
|
|
1612
1630
|
return { nonce, s2: decodeSig(s2), msg };
|
|
1613
1631
|
},
|
|
@@ -1622,7 +1640,13 @@ function genFalcon(opts) {
|
|
|
1622
1640
|
return headerCoder(0x30 + logn, splitCoder('falcon.detachedSignature', NONCELEN, getSigLen(s2))).encode([nonce, opts.padded ? pad(sigLen).encode(s2) : s2]);
|
|
1623
1641
|
},
|
|
1624
1642
|
decode(data) {
|
|
1625
|
-
|
|
1643
|
+
// Padded detached signatures are fixed-length (`lengths.signature`), so the payload width
|
|
1644
|
+
// must come from the parameter set, not from the input: deriving it would accept appended
|
|
1645
|
+
// zero bytes and truncated padding as extra valid encodings of the same signature.
|
|
1646
|
+
// Unpadded signatures are variable-length; decodeUnpaddedSig() enforces the exact canonical
|
|
1647
|
+
// bitlength of whatever remains.
|
|
1648
|
+
const payloadLen = opts.padded ? sigLen : data.length - NONCELEN - 1;
|
|
1649
|
+
const [nonce, raw] = headerCoder(0x30 + logn, splitCoder('falcon.detachedSignature', NONCELEN, payloadLen)).decode(data);
|
|
1626
1650
|
const s2 = decodeSig(raw);
|
|
1627
1651
|
return { nonce, s2 };
|
|
1628
1652
|
},
|
|
@@ -1710,7 +1734,7 @@ function genFalcon(opts) {
|
|
|
1710
1734
|
shakeBuf;
|
|
1711
1735
|
ctrView;
|
|
1712
1736
|
// ChaCha
|
|
1713
|
-
ctr =
|
|
1737
|
+
ctr = _0n;
|
|
1714
1738
|
buf;
|
|
1715
1739
|
buf32;
|
|
1716
1740
|
pos;
|
|
@@ -1760,8 +1784,8 @@ function genFalcon(opts) {
|
|
|
1760
1784
|
const out32 = swap32IfBE(this.buf32);
|
|
1761
1785
|
for (let i = 0; i < 8; i++, this.ctr++) {
|
|
1762
1786
|
const n = swap32IfBE(this.nonce32.slice()); // [n0, n1, n2, n3]
|
|
1763
|
-
n[2] ^= Number(this.ctr &
|
|
1764
|
-
n[3] ^= Number(this.ctr >>
|
|
1787
|
+
n[2] ^= Number(this.ctr & MASK_32n);
|
|
1788
|
+
n[3] ^= Number(this.ctr >> _32n);
|
|
1765
1789
|
// chacha20() takes raw nonce bytes; on BE the word-normalized temp must be swapped back.
|
|
1766
1790
|
swap32IfBE(n.subarray(1));
|
|
1767
1791
|
chacha20(this.key, u8(n.subarray(1)), EMPTY_CHACHA20_BLOCK, this.curBlock, n[0]);
|
|
@@ -2078,7 +2102,8 @@ function genFalcon(opts) {
|
|
|
2078
2102
|
// ▷ Remove 1 byte for the header, and 40 bytes for r
|
|
2079
2103
|
// 11: while (s = ⊥)
|
|
2080
2104
|
// 12: return sig = (r, s)
|
|
2081
|
-
abytes(msg);
|
|
2105
|
+
abytes(msg, undefined, 'msg');
|
|
2106
|
+
abytes(sk, secretKeyCoder.bytesLen, 'secretKey');
|
|
2082
2107
|
// One RNG stream drives both the public 40-byte nonce and the 48-byte sampler seed, so
|
|
2083
2108
|
// deterministic rnd hooks make signatures deterministic for fixed secretKey/message inputs.
|
|
2084
2109
|
const nonce = rnd(40);
|
|
@@ -2184,6 +2209,8 @@ function genFalcon(opts) {
|
|
|
2184
2209
|
validateSigOpts(opts);
|
|
2185
2210
|
if (opts.context !== undefined)
|
|
2186
2211
|
throw new Error('context is not supported');
|
|
2212
|
+
if (opts.random !== undefined && typeof opts.random !== 'function')
|
|
2213
|
+
throw new TypeError('"opts.random" expected function, got type=' + typeof opts.random);
|
|
2187
2214
|
if (opts.random !== undefined)
|
|
2188
2215
|
return opts.random;
|
|
2189
2216
|
if (opts.extraEntropy === undefined)
|
|
@@ -2223,6 +2250,7 @@ function genFalcon(opts) {
|
|
|
2223
2250
|
return { publicKey: pk, secretKey: sk };
|
|
2224
2251
|
};
|
|
2225
2252
|
const getPublicKey = (sk) => {
|
|
2253
|
+
abytes(sk, secretKeyCoder.bytesLen, 'secretKey');
|
|
2226
2254
|
const [f, g, F] = secretKeyCoder.decode(sk);
|
|
2227
2255
|
try {
|
|
2228
2256
|
const h = computePublic(f, g);
|
|
@@ -2245,9 +2273,10 @@ function genFalcon(opts) {
|
|
|
2245
2273
|
*/
|
|
2246
2274
|
const verify = (sig, msg, pk, verOpts = {}) => {
|
|
2247
2275
|
checkVerOpts(verOpts);
|
|
2248
|
-
abytes(sig);
|
|
2249
|
-
abytes(msg);
|
|
2250
|
-
|
|
2276
|
+
abytes(sig, undefined, 'signature');
|
|
2277
|
+
abytes(msg, undefined, 'msg');
|
|
2278
|
+
// Length/canonical public-key failures are decoded below and return false; only type is fatal.
|
|
2279
|
+
abytes(pk, undefined, 'publicKey');
|
|
2251
2280
|
try {
|
|
2252
2281
|
const { s2, nonce } = SignatureCoderDetached(logn).decode(sig);
|
|
2253
2282
|
return verifyRaw(pk, s2, nonce, msg);
|
|
@@ -2382,4 +2411,3 @@ export const __tests = /* @__PURE__ */ (() => Object.freeze({
|
|
|
2382
2411
|
falcon1024: falcon1024.__test,
|
|
2383
2412
|
falcon1024padded: falcon1024padded.__test,
|
|
2384
2413
|
}))();
|
|
2385
|
-
//# sourceMappingURL=falcon.js.map
|
package/hybrid.d.ts
CHANGED
|
@@ -190,7 +190,11 @@ expandSeed: TArg<ExpandSeed>, combiner: TArg<Combiner>, ...kems: TArg<KEM[]>): T
|
|
|
190
190
|
* import { combineSigners, expandSeedXof } from '@noble/post-quantum/hybrid.js';
|
|
191
191
|
* import { ml_dsa44 } from '@noble/post-quantum/ml-dsa.js';
|
|
192
192
|
* const hybrid = combineSigners(32, expandSeedXof(shake256), ml_dsa44, ml_dsa44);
|
|
193
|
-
* const
|
|
193
|
+
* const seed = new Uint8Array(hybrid.lengths.seed!).fill(1);
|
|
194
|
+
* const { secretKey, publicKey } = hybrid.keygen(seed);
|
|
195
|
+
* const msg = new TextEncoder().encode('hello noble');
|
|
196
|
+
* const sig = hybrid.sign(msg, secretKey);
|
|
197
|
+
* const isValid = hybrid.verify(sig, msg, publicKey);
|
|
194
198
|
* ```
|
|
195
199
|
*/
|
|
196
200
|
export declare function combineSigners(realSeedLen: number | undefined, expandSeed: TArg<ExpandSeed>, ...signers: TArg<Signer[]>): TRet<Signer>;
|
|
@@ -263,19 +267,4 @@ export declare const ml_kem768_x25519: TRet<KEM>;
|
|
|
263
267
|
export declare const ml_kem768_p256: TRet<KEM>;
|
|
264
268
|
/** P-384 + ML-KEM-1024 hybrid preset. */
|
|
265
269
|
export declare const ml_kem1024_p384: TRet<KEM>;
|
|
266
|
-
/** Legacy alias for `ml_kem768_x25519`. */
|
|
267
|
-
export declare const XWing: TRet<KEM>;
|
|
268
|
-
/** Legacy alias for `ml_kem768_x25519`. */
|
|
269
|
-
export declare const MLKEM768X25519: TRet<KEM>;
|
|
270
|
-
/** Legacy alias for `ml_kem768_p256`. */
|
|
271
|
-
export declare const MLKEM768P256: TRet<KEM>;
|
|
272
|
-
/** Legacy alias for `ml_kem1024_p384`. */
|
|
273
|
-
export declare const MLKEM1024P384: TRet<KEM>;
|
|
274
|
-
/** Legacy alias for `QSF_ml_kem768_p256`. */
|
|
275
|
-
export declare const QSFMLKEM768P256: TRet<KEM>;
|
|
276
|
-
/** Legacy alias for `QSF_ml_kem1024_p384`. */
|
|
277
|
-
export declare const QSFMLKEM1024P384: TRet<KEM>;
|
|
278
|
-
/** Legacy alias for `KitchenSink_ml_kem768_x25519`. */
|
|
279
|
-
export declare const KitchenSinkMLKEM768X25519: TRet<KEM>;
|
|
280
270
|
export {};
|
|
281
|
-
//# sourceMappingURL=hybrid.d.ts.map
|
package/hybrid.js
CHANGED
|
@@ -79,15 +79,38 @@ import {} from '@noble/curves/abstract/montgomery.js';
|
|
|
79
79
|
import {} 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
|
-
import { asciiToBytes, bytesToNumberBE, bytesToNumberLE, concatBytes, numberToBytesBE, } from '@noble/curves/utils.js';
|
|
82
|
+
import { abool, afunction, asciiToBytes, bytesToNumberBE, bytesToNumberLE, concatBytes, numberToBytesBE, } from '@noble/curves/utils.js';
|
|
83
83
|
import { expand, extract } from '@noble/hashes/hkdf.js';
|
|
84
84
|
import { sha256 } from '@noble/hashes/sha2.js';
|
|
85
85
|
import { sha3_256, shake256 } from '@noble/hashes/sha3.js';
|
|
86
86
|
import { abytes, ahash, anumber } from '@noble/hashes/utils.js';
|
|
87
87
|
import { ml_kem1024, ml_kem768 } from "./ml-kem.js";
|
|
88
|
-
import { cleanBytes, copyBytes, randomBytes, splitCoder, validateSigOpts, validateVerOpts, } from "./utils.js";
|
|
88
|
+
import { aobject, astring, cleanBytes, copyBytes, randomBytes, splitCoder, validateSigOpts, validateVerOpts, } from "./utils.js";
|
|
89
|
+
const validateKEM = (kem, title) => {
|
|
90
|
+
const k = aobject(kem, title);
|
|
91
|
+
aobject(k.lengths, `${title}.lengths`);
|
|
92
|
+
afunction(k.keygen, `${title}.keygen`);
|
|
93
|
+
afunction(k.getPublicKey, `${title}.getPublicKey`);
|
|
94
|
+
afunction(k.encapsulate, `${title}.encapsulate`);
|
|
95
|
+
afunction(k.decapsulate, `${title}.decapsulate`);
|
|
96
|
+
return k;
|
|
97
|
+
};
|
|
98
|
+
const validateSigner = (signer, title) => {
|
|
99
|
+
const s = aobject(signer, title);
|
|
100
|
+
aobject(s.lengths, `${title}.lengths`);
|
|
101
|
+
afunction(s.keygen, `${title}.keygen`);
|
|
102
|
+
afunction(s.getPublicKey, `${title}.getPublicKey`);
|
|
103
|
+
afunction(s.sign, `${title}.sign`);
|
|
104
|
+
afunction(s.verify, `${title}.verify`);
|
|
105
|
+
return s;
|
|
106
|
+
};
|
|
89
107
|
// Can re-use if decide to signatures support, on other hand getSecretKey is specific and ugly
|
|
90
108
|
function ecKeygen(curve, allowZeroKey = false) {
|
|
109
|
+
const c = aobject(curve, 'curve');
|
|
110
|
+
aobject(c.lengths, 'curve.lengths');
|
|
111
|
+
afunction(c.keygen, 'curve.keygen');
|
|
112
|
+
afunction(c.getPublicKey, 'curve.getPublicKey');
|
|
113
|
+
abool(allowZeroKey, 'allowZeroKey');
|
|
91
114
|
const lengths = curve.lengths;
|
|
92
115
|
let keygen = curve.keygen;
|
|
93
116
|
if (allowZeroKey) {
|
|
@@ -149,6 +172,11 @@ export function ecdhKem(curve, allowZeroKey = false) {
|
|
|
149
172
|
const kg = ecKeygen(curve, allowZeroKey);
|
|
150
173
|
if (!curve.getSharedSecret)
|
|
151
174
|
throw new Error('wrong curve'); // ed25519 doesn't have one!
|
|
175
|
+
// Standalone (not `this.decapsulate`) so encapsulate works even when methods are destructured.
|
|
176
|
+
const decapsulate = (cipherText, secretKey) => {
|
|
177
|
+
const res = curve.getSharedSecret(secretKey, cipherText);
|
|
178
|
+
return (curve.lengths.publicKeyHasPrefix ? res.subarray(1) : res);
|
|
179
|
+
};
|
|
152
180
|
return {
|
|
153
181
|
lengths: { ...kg.lengths, msg: kg.lengths.seed, cipherText: kg.lengths.publicKey },
|
|
154
182
|
keygen: kg.keygen,
|
|
@@ -159,8 +187,8 @@ export function ecdhKem(curve, allowZeroKey = false) {
|
|
|
159
187
|
const seed = copyBytes(rand);
|
|
160
188
|
let ek = undefined;
|
|
161
189
|
try {
|
|
162
|
-
ek =
|
|
163
|
-
const sharedSecret =
|
|
190
|
+
ek = kg.keygen(seed).secretKey;
|
|
191
|
+
const sharedSecret = decapsulate(publicKey, ek);
|
|
164
192
|
const cipherText = curve.getPublicKey(ek);
|
|
165
193
|
return { sharedSecret, cipherText };
|
|
166
194
|
}
|
|
@@ -172,10 +200,7 @@ export function ecdhKem(curve, allowZeroKey = false) {
|
|
|
172
200
|
cleanBytes(ek);
|
|
173
201
|
}
|
|
174
202
|
},
|
|
175
|
-
decapsulate
|
|
176
|
-
const res = curve.getSharedSecret(secretKey, cipherText);
|
|
177
|
-
return (curve.lengths.publicKeyHasPrefix ? res.subarray(1) : res);
|
|
178
|
-
},
|
|
203
|
+
decapsulate,
|
|
179
204
|
};
|
|
180
205
|
}
|
|
181
206
|
/**
|
|
@@ -303,26 +328,43 @@ expandSeed_, ...ck_) {
|
|
|
303
328
|
cleanBytes(secretKey);
|
|
304
329
|
}
|
|
305
330
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
const { publicKey: pk, secretKey } = expandDecapsulationKey(seed);
|
|
331
|
+
// Standalone (not a method) so getPublicKey / destructured usage never depends on `this`.
|
|
332
|
+
const keygen = (seed) => {
|
|
333
|
+
// Detach the root: the exported secretKey must not alias caller-owned seed bytes, so later
|
|
334
|
+
// caller mutation of the seed cannot silently change the secret key (and vice versa).
|
|
335
|
+
const root = seed === undefined ? randomBytes(realSeedLen) : copyBytes(seed);
|
|
336
|
+
let res;
|
|
337
|
+
try {
|
|
338
|
+
const { publicKey: pk, secretKey } = expandDecapsulationKey(root);
|
|
315
339
|
try {
|
|
316
|
-
|
|
317
|
-
|
|
340
|
+
res = {
|
|
341
|
+
secretKey: root,
|
|
342
|
+
publicKey: pkCoder.encode(pk),
|
|
343
|
+
};
|
|
318
344
|
}
|
|
319
345
|
finally {
|
|
320
|
-
|
|
321
|
-
// The exported secretKey is the caller/root seed itself; child secret keys are internal
|
|
346
|
+
// The exported secretKey is the (detached) root seed; child secret keys are internal
|
|
322
347
|
// expansion outputs that are cleaned whether encoding succeeds or throws.
|
|
323
|
-
cleanBytes(secretKey);
|
|
348
|
+
cleanBytes(pk, secretKey);
|
|
324
349
|
}
|
|
350
|
+
return res;
|
|
351
|
+
}
|
|
352
|
+
finally {
|
|
353
|
+
if (!res)
|
|
354
|
+
cleanBytes(root);
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
return {
|
|
358
|
+
info: { lengths: { seed: realSeedLen, publicKey: pkCoder.bytesLen, secretKey: realSeedLen } },
|
|
359
|
+
// Composite secret keys are root seeds, so public-key derivation reruns key expansion from
|
|
360
|
+
// that seed instead of decoding a packed child-secret-key structure.
|
|
361
|
+
getPublicKey: (secretKey) => {
|
|
362
|
+
const keys = keygen(secretKey);
|
|
363
|
+
// keygen detaches its exported root; getPublicKey discards that half of the result.
|
|
364
|
+
cleanBytes(keys.secretKey);
|
|
365
|
+
return keys.publicKey;
|
|
325
366
|
},
|
|
367
|
+
keygen,
|
|
326
368
|
expandDecapsulationKey,
|
|
327
369
|
realSeedLen,
|
|
328
370
|
};
|
|
@@ -356,15 +398,25 @@ expandSeed_, ...ck_) {
|
|
|
356
398
|
export function combineKEMS(realSeedLen, // how much bytes expandSeed expects
|
|
357
399
|
realMsgLen, // how much bytes combiner returns
|
|
358
400
|
expandSeed, combiner, ...kems) {
|
|
401
|
+
if (realSeedLen !== undefined)
|
|
402
|
+
anumber(realSeedLen, 'realSeedLen');
|
|
403
|
+
if (realMsgLen !== undefined)
|
|
404
|
+
anumber(realMsgLen, 'realMsgLen');
|
|
405
|
+
if (typeof expandSeed !== 'function')
|
|
406
|
+
throw new TypeError('"expandSeed" expected function, got type=' + typeof expandSeed);
|
|
407
|
+
if (typeof combiner !== 'function')
|
|
408
|
+
throw new TypeError('"combiner" expected function, got type=' + typeof combiner);
|
|
359
409
|
const rawCombiner = combiner;
|
|
360
410
|
const rawKems = kems;
|
|
411
|
+
for (let i = 0; i < rawKems.length; i++)
|
|
412
|
+
validateKEM(rawKems[i], `kems[${i}]`);
|
|
361
413
|
const keys = combineKeys(realSeedLen, expandSeed, ...rawKems);
|
|
362
414
|
const ctCoder = splitLengths(rawKems, 'cipherText');
|
|
363
415
|
const pkCoder = splitLengths(rawKems, 'publicKey');
|
|
364
416
|
const msgCoder = splitLengths(rawKems, 'msg');
|
|
365
417
|
if (realMsgLen === undefined)
|
|
366
418
|
realMsgLen = msgCoder.bytesLen;
|
|
367
|
-
anumber(realMsgLen);
|
|
419
|
+
anumber(realMsgLen, 'realMsgLen');
|
|
368
420
|
const lengths = Object.freeze({
|
|
369
421
|
...keys.info.lengths,
|
|
370
422
|
msg: realMsgLen,
|
|
@@ -431,11 +483,21 @@ expandSeed, combiner, ...kems) {
|
|
|
431
483
|
* import { combineSigners, expandSeedXof } from '@noble/post-quantum/hybrid.js';
|
|
432
484
|
* import { ml_dsa44 } from '@noble/post-quantum/ml-dsa.js';
|
|
433
485
|
* const hybrid = combineSigners(32, expandSeedXof(shake256), ml_dsa44, ml_dsa44);
|
|
434
|
-
* const
|
|
486
|
+
* const seed = new Uint8Array(hybrid.lengths.seed!).fill(1);
|
|
487
|
+
* const { secretKey, publicKey } = hybrid.keygen(seed);
|
|
488
|
+
* const msg = new TextEncoder().encode('hello noble');
|
|
489
|
+
* const sig = hybrid.sign(msg, secretKey);
|
|
490
|
+
* const isValid = hybrid.verify(sig, msg, publicKey);
|
|
435
491
|
* ```
|
|
436
492
|
*/
|
|
437
493
|
export function combineSigners(realSeedLen, expandSeed, ...signers) {
|
|
494
|
+
if (realSeedLen !== undefined)
|
|
495
|
+
anumber(realSeedLen, 'realSeedLen');
|
|
496
|
+
if (typeof expandSeed !== 'function')
|
|
497
|
+
throw new TypeError('"expandSeed" expected function, got type=' + typeof expandSeed);
|
|
438
498
|
const rawSigners = signers;
|
|
499
|
+
for (let i = 0; i < rawSigners.length; i++)
|
|
500
|
+
validateSigner(rawSigners[i], `signers[${i}]`);
|
|
439
501
|
const keys = combineKeys(realSeedLen, expandSeed, ...rawSigners);
|
|
440
502
|
const sigCoder = splitLengths(rawSigners, 'signature');
|
|
441
503
|
const pkCoder = splitLengths(rawSigners, 'publicKey');
|
|
@@ -464,14 +526,21 @@ export function combineSigners(realSeedLen, expandSeed, ...signers) {
|
|
|
464
526
|
}
|
|
465
527
|
},
|
|
466
528
|
/** Verify one combined signature.
|
|
467
|
-
*
|
|
468
|
-
*
|
|
529
|
+
* Wrong-length aggregate signatures return `false` (matching ml-dsa / slh-dsa behavior), as
|
|
530
|
+
* does any failing child verify. Throws on unsupported generic opts or malformed publicKey.
|
|
469
531
|
*/
|
|
470
532
|
verify: (signature, message, publicKey, opts = {}) => {
|
|
471
533
|
validateVerOpts(opts);
|
|
472
534
|
if (opts.context !== undefined)
|
|
473
535
|
throw new Error('combineSigners does not support context; use the underlying signer directly');
|
|
536
|
+
// Malformed signature *length* is a verification failure, not a thrown type error —
|
|
537
|
+
// consistent with ml-dsa / slh-dsa. Must run before sigCoder.decode, which throws.
|
|
538
|
+
// Preserve TypeError for non-byte API arguments before treating byte lengths as invalid.
|
|
539
|
+
abytes(signature, undefined, 'signature');
|
|
540
|
+
// A signature failure must not hide malformed aggregate public-key bytes.
|
|
474
541
|
const pks = pkCoder.decode(publicKey);
|
|
542
|
+
if (signature.length !== sigCoder.bytesLen)
|
|
543
|
+
return false;
|
|
475
544
|
const sigs = sigCoder.decode(signature);
|
|
476
545
|
for (let i = 0; i < rawSigners.length; i++) {
|
|
477
546
|
if (!rawSigners[i].verify(sigs[i], message, pks[i]))
|
|
@@ -506,7 +575,14 @@ export function combineSigners(realSeedLen, expandSeed, ...signers) {
|
|
|
506
575
|
* ```
|
|
507
576
|
*/
|
|
508
577
|
export function QSF(label, pqc, curveKEM, xof, kdf) {
|
|
578
|
+
astring(label, 'label');
|
|
579
|
+
validateKEM(pqc, 'pqc');
|
|
580
|
+
validateKEM(curveKEM, 'curveKEM');
|
|
581
|
+
if (typeof xof !== 'function' || typeof xof.create !== 'function')
|
|
582
|
+
throw new TypeError('"xof" expected hash function, got type=' + typeof xof);
|
|
509
583
|
ahash(xof);
|
|
584
|
+
if (typeof kdf !== 'function' || typeof kdf.create !== 'function')
|
|
585
|
+
throw new TypeError('"kdf" expected hash function, got type=' + typeof kdf);
|
|
510
586
|
ahash(kdf);
|
|
511
587
|
return combineKEMS(32, kdf.outputLen, expandSeedXof(xof), (pk, ct, ss) => kdf(concatBytes(ss[0], ss[1], ct[1], pk[1], asciiToBytes(label))), pqc, curveKEM);
|
|
512
588
|
}
|
|
@@ -541,7 +617,14 @@ export const QSF_ml_kem1024_p384 = /* @__PURE__ */ (() => QSF('QSF-KEM(ML-KEM-10
|
|
|
541
617
|
* ```
|
|
542
618
|
*/
|
|
543
619
|
export function createKitchenSink(label, pqc, curveKEM, xof, hash) {
|
|
620
|
+
astring(label, 'label');
|
|
621
|
+
validateKEM(pqc, 'pqc');
|
|
622
|
+
validateKEM(curveKEM, 'curveKEM');
|
|
623
|
+
if (typeof xof !== 'function' || typeof xof.create !== 'function')
|
|
624
|
+
throw new TypeError('"xof" expected hash function, got type=' + typeof xof);
|
|
544
625
|
ahash(xof);
|
|
626
|
+
if (typeof hash !== 'function' || typeof hash.create !== 'function')
|
|
627
|
+
throw new TypeError('"hash" expected hash function, got type=' + typeof hash);
|
|
545
628
|
ahash(hash);
|
|
546
629
|
return combineKEMS(32, 32, expandSeedXof(xof), (pk, ct, ss) => {
|
|
547
630
|
const preimage = concatBytes(ss[0], ss[1], ct[0], pk[0], ct[1], pk[1], asciiToBytes(label));
|
|
@@ -596,6 +679,11 @@ function nistCurveKem(curve, scalarLen, elemLen, nseed) {
|
|
|
596
679
|
const publicKey = curve.getPublicKey(secretKey, false);
|
|
597
680
|
return { secretKey, publicKey };
|
|
598
681
|
}
|
|
682
|
+
// Standalone (not `this.decapsulate`) so encapsulate works even when methods are destructured.
|
|
683
|
+
const decapsulate = (cipherText, secretKey) => {
|
|
684
|
+
const full = curve.getSharedSecret(secretKey, cipherText);
|
|
685
|
+
return full.subarray(1);
|
|
686
|
+
};
|
|
599
687
|
return {
|
|
600
688
|
lengths: {
|
|
601
689
|
secretKey: scalarLen,
|
|
@@ -616,7 +704,7 @@ function nistCurveKem(curve, scalarLen, elemLen, nseed) {
|
|
|
616
704
|
let ek = undefined;
|
|
617
705
|
try {
|
|
618
706
|
ek = rejectionSampling(rand).secretKey;
|
|
619
|
-
const sharedSecret =
|
|
707
|
+
const sharedSecret = decapsulate(publicKey, ek);
|
|
620
708
|
const cipherText = curve.getPublicKey(ek, false);
|
|
621
709
|
return { sharedSecret, cipherText };
|
|
622
710
|
}
|
|
@@ -627,10 +715,7 @@ function nistCurveKem(curve, scalarLen, elemLen, nseed) {
|
|
|
627
715
|
cleanBytes(ek);
|
|
628
716
|
}
|
|
629
717
|
},
|
|
630
|
-
decapsulate
|
|
631
|
-
const full = curve.getSharedSecret(secretKey, cipherText);
|
|
632
|
-
return full.subarray(1);
|
|
633
|
-
},
|
|
718
|
+
decapsulate,
|
|
634
719
|
};
|
|
635
720
|
}
|
|
636
721
|
/**
|
|
@@ -650,29 +735,14 @@ function concreteHybridKem(label, mlkem, curve, nseed) {
|
|
|
650
735
|
const totalSeedLen = mlkemSeedLen + nseed;
|
|
651
736
|
return combineKEMS(32, 32, (seed) => {
|
|
652
737
|
abytes(seed, 32);
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
738
|
+
// One SHAKE256 stream split by the seed coder as mlkemSeed (64) || curveSeed (nseed).
|
|
739
|
+
// Returned directly: the previous concatBytes of two adjacent subarrays produced an
|
|
740
|
+
// identical copy while leaving this original buffer unwiped; expandDecapsulationKey
|
|
741
|
+
// wipes the returned buffer after the child seeds are copied out.
|
|
742
|
+
return shake256(seed, { dkLen: totalSeedLen });
|
|
657
743
|
}, (pk, ct, ss) => sha3_256(concatBytes(ss[0], ss[1], ct[1], pk[1], asciiToBytes(label))), mlkem, curveKem);
|
|
658
744
|
}
|
|
659
745
|
/** P-256 + ML-KEM-768 hybrid preset. */
|
|
660
746
|
export const ml_kem768_p256 = /* @__PURE__ */ (() => concreteHybridKem('MLKEM768-P256', ml_kem768, p256, 128))();
|
|
661
747
|
/** P-384 + ML-KEM-1024 hybrid preset. */
|
|
662
748
|
export const ml_kem1024_p384 = /* @__PURE__ */ (() => concreteHybridKem('MLKEM1024-P384', ml_kem1024, p384, 48))();
|
|
663
|
-
// Legacy aliases
|
|
664
|
-
/** Legacy alias for `ml_kem768_x25519`. */
|
|
665
|
-
export const XWing = /* @__PURE__ */ (() => ml_kem768_x25519)();
|
|
666
|
-
/** Legacy alias for `ml_kem768_x25519`. */
|
|
667
|
-
export const MLKEM768X25519 = /* @__PURE__ */ (() => ml_kem768_x25519)();
|
|
668
|
-
/** Legacy alias for `ml_kem768_p256`. */
|
|
669
|
-
export const MLKEM768P256 = /* @__PURE__ */ (() => ml_kem768_p256)();
|
|
670
|
-
/** Legacy alias for `ml_kem1024_p384`. */
|
|
671
|
-
export const MLKEM1024P384 = /* @__PURE__ */ (() => ml_kem1024_p384)();
|
|
672
|
-
/** Legacy alias for `QSF_ml_kem768_p256`. */
|
|
673
|
-
export const QSFMLKEM768P256 = /* @__PURE__ */ (() => QSF_ml_kem768_p256)();
|
|
674
|
-
/** Legacy alias for `QSF_ml_kem1024_p384`. */
|
|
675
|
-
export const QSFMLKEM1024P384 = /* @__PURE__ */ (() => QSF_ml_kem1024_p384)();
|
|
676
|
-
/** Legacy alias for `KitchenSink_ml_kem768_x25519`. */
|
|
677
|
-
export const KitchenSinkMLKEM768X25519 = /* @__PURE__ */ (() => KitchenSink_ml_kem768_x25519)();
|
|
678
|
-
//# sourceMappingURL=hybrid.js.map
|
package/index.d.ts
CHANGED
package/index.js
CHANGED