@noble/post-quantum 0.7.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +90 -16
- package/_crystals.js +1 -1
- package/falcon.d.ts +1 -1
- package/falcon.js +121 -62
- package/hybrid.d.ts +33 -12
- package/hybrid.js +100 -41
- package/index.js +1 -1
- package/ml-dsa.d.ts +3 -3
- package/ml-dsa.js +57 -20
- package/ml-kem.js +85 -34
- package/package.json +15 -11
- package/slh-dsa.js +34 -11
- package/src/_crystals.ts +1 -1
- package/src/falcon.ts +127 -70
- package/src/hybrid.ts +108 -39
- package/src/index.ts +1 -1
- package/src/ml-dsa.ts +70 -25
- package/src/ml-kem.ts +76 -35
- package/src/slh-dsa.ts +44 -13
- package/src/utils.ts +115 -10
- package/src/webcrypto.ts +322 -0
- package/utils.d.ts +36 -2
- package/utils.js +105 -12
- package/webcrypto.d.ts +91 -0
- package/webcrypto.js +213 -0
package/src/ml-kem.ts
CHANGED
|
@@ -222,7 +222,7 @@ function SampleNTT(xof_: TArg<XofGet>): TRet<Poly> {
|
|
|
222
222
|
// The reader must already bind the Algorithm 7 seed||j||i bytes
|
|
223
223
|
// and return block lengths divisible by 3.
|
|
224
224
|
const r: Poly = new Uint16Array(N);
|
|
225
|
-
for (let j = 0; j < N;
|
|
225
|
+
for (let j = 0; j < N;) {
|
|
226
226
|
const b = xof();
|
|
227
227
|
if (b.length % 3) throw new Error('SampleNTT: unaligned block');
|
|
228
228
|
for (let i = 0; j < N && i + 3 <= b.length; i += 3) {
|
|
@@ -399,8 +399,11 @@ const genKPKE = (opts_: TArg<KyberOpts>) => {
|
|
|
399
399
|
// tmp += sk[i] * u[i]
|
|
400
400
|
for (let i = 0; i < K; i++) polyAdd(tmp, MultiplyNTTs(sk[i], crystals.NTT.encode(u[i])));
|
|
401
401
|
polySub(v, crystals.NTT.decode(tmp)); // w = v' - tmp
|
|
402
|
-
|
|
403
|
-
|
|
402
|
+
// `v` now holds w, from which the plaintext is just a 1-bit threshold away, so wipe it too.
|
|
403
|
+
// encode() allocates its own buffer, so the returned bytes do not alias `v`.
|
|
404
|
+
const res = poly1.encode(v) as TRet<Uint8Array>;
|
|
405
|
+
cleanBytes(tmp, sk, u, v);
|
|
406
|
+
return res;
|
|
404
407
|
},
|
|
405
408
|
};
|
|
406
409
|
};
|
|
@@ -409,6 +412,11 @@ const genKPKE = (opts_: TArg<KyberOpts>) => {
|
|
|
409
412
|
* Public ML-KEM wrapper over the internal K-PKE subroutine.
|
|
410
413
|
* `keygen(seed)` and `encapsulate(publicKey, msg)` are deterministic/test-oriented hooks that map
|
|
411
414
|
* more directly to Algorithms 16-17 than to the pure no-input / random-internal Algorithms 19-20.
|
|
415
|
+
* `encapsulate`'s optional `msg` is the 32-byte message randomness `m` of Algorithm 17, the
|
|
416
|
+
* pre-image the shared secret is derived from, NOT a plaintext to encrypt: ML-KEM is a key
|
|
417
|
+
* encapsulation mechanism, not a cipher. Omit it to draw fresh randomness; pass it only to
|
|
418
|
+
* reproduce a known-answer vector, and only as 32 uniformly random bytes, since a low-entropy or
|
|
419
|
+
* reused value makes the shared secret predictable. The same holds for `keygen`'s optional `seed`.
|
|
412
420
|
* decapsulate() tries to follow the Algorithms 18/21 implicit-reject structure as closely as
|
|
413
421
|
* practical here by re-encrypting, comparing ciphertexts, returning `Khat` on match or `Kbar` on
|
|
414
422
|
* mismatch, and zeroizing the non-returned shared-secret candidate; JS/JIT still provides no
|
|
@@ -443,34 +451,58 @@ function createKyber(opts: TArg<KyberOpts>): TRet<MLKEM> {
|
|
|
443
451
|
return Object.freeze({
|
|
444
452
|
info: Object.freeze({ type: 'ml-kem' }),
|
|
445
453
|
lengths: kemLengths,
|
|
446
|
-
keygen: (seed
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
454
|
+
keygen: (seed?: TArg<Uint8Array>) => {
|
|
455
|
+
// A generated seed carries z (the implicit-rejection secret) and must be wiped once the
|
|
456
|
+
// secret key holds a copy, matching ml-dsa / slh-dsa / falcon keygen. A caller-supplied
|
|
457
|
+
// seed is the caller's to manage (and the immutability test requires it stay untouched).
|
|
458
|
+
const ownSeed = seed === undefined;
|
|
459
|
+
const s = ownSeed ? randomBytes(seedLen) : (seed as TArg<Uint8Array>);
|
|
460
|
+
let sk: Uint8Array | undefined;
|
|
461
|
+
let publicKeyHash: Uint8Array | undefined;
|
|
462
|
+
try {
|
|
463
|
+
abytes(s, seedLen, 'seed');
|
|
464
|
+
const keys = KPKE.keygen(s.subarray(0, 32));
|
|
465
|
+
const publicKey = keys.publicKey;
|
|
466
|
+
sk = keys.secretKey as Uint8Array;
|
|
467
|
+
publicKeyHash = HASH256(publicKey);
|
|
468
|
+
// (dkPKE||ek||H(ek)||z)
|
|
469
|
+
const secretKey = secretCoder.encode([sk, publicKey, publicKeyHash, s.subarray(32)]);
|
|
470
|
+
return {
|
|
471
|
+
publicKey: publicKey as TRet<Uint8Array>,
|
|
472
|
+
secretKey: secretKey as TRet<Uint8Array>,
|
|
473
|
+
};
|
|
474
|
+
} finally {
|
|
475
|
+
if (sk !== undefined) cleanBytes(sk);
|
|
476
|
+
if (publicKeyHash !== undefined) cleanBytes(publicKeyHash);
|
|
477
|
+
if (ownSeed) cleanBytes(s);
|
|
478
|
+
}
|
|
457
479
|
},
|
|
458
480
|
getPublicKey: (secretKey: TArg<Uint8Array>): TRet<Uint8Array> => {
|
|
459
481
|
const [_sk, publicKey, _publicKeyHash, _z] = secretCoder.decode(secretKey);
|
|
460
482
|
return Uint8Array.from(publicKey) as TRet<Uint8Array>;
|
|
461
483
|
},
|
|
462
|
-
encapsulate: (publicKey: TArg<Uint8Array>, msg
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
const
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
484
|
+
encapsulate: (publicKey: TArg<Uint8Array>, msg?: TArg<Uint8Array>) => {
|
|
485
|
+
// A generated message is the preimage of the shared secret (K = G(m || H(ek))[0:32]) and
|
|
486
|
+
// must be wiped. A caller-supplied message is the deterministic-randomness hook and the
|
|
487
|
+
// caller's to manage (the immutability test requires it stay untouched).
|
|
488
|
+
const ownMsg = msg === undefined;
|
|
489
|
+
const m = ownMsg ? randomBytes(msgLen) : (msg as TArg<Uint8Array>);
|
|
490
|
+
let kr: Uint8Array | undefined;
|
|
491
|
+
try {
|
|
492
|
+
abytes(publicKey, lengths.publicKey, 'publicKey');
|
|
493
|
+
abytes(m, msgLen, 'message');
|
|
494
|
+
validateModulus(publicKey, 'encapsulate');
|
|
495
|
+
// derive randomness
|
|
496
|
+
kr = HASH512.create().update(m).update(HASH256(publicKey)).digest();
|
|
497
|
+
const cipherText = KPKE.encrypt(publicKey, m, kr.subarray(32, 64));
|
|
498
|
+
return {
|
|
499
|
+
cipherText: cipherText as TRet<Uint8Array>,
|
|
500
|
+
sharedSecret: kr.subarray(0, 32) as TRet<Uint8Array>,
|
|
501
|
+
};
|
|
502
|
+
} finally {
|
|
503
|
+
if (kr !== undefined) cleanBytes(kr.subarray(32));
|
|
504
|
+
if (ownMsg) cleanBytes(m);
|
|
505
|
+
}
|
|
474
506
|
},
|
|
475
507
|
decapsulate: (cipherText: TArg<Uint8Array>, secretKey: TArg<Uint8Array>): TRet<Uint8Array> => {
|
|
476
508
|
abytes(secretKey, secretCoder.bytesLen, 'secretKey'); // 768*k + 96
|
|
@@ -509,15 +541,24 @@ function createKyber(opts: TArg<KyberOpts>): TRet<MLKEM> {
|
|
|
509
541
|
const cached = KPKE.prepare(ek);
|
|
510
542
|
return Object.freeze({
|
|
511
543
|
publicKey: ek as TRet<Uint8Array>,
|
|
512
|
-
encapsulate: (msg
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
const
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
544
|
+
encapsulate: (msg?: TArg<Uint8Array>) => {
|
|
545
|
+
// As in the non-prepared encapsulate: a generated message is the shared-secret
|
|
546
|
+
// preimage and is wiped; a caller-supplied one is left untouched.
|
|
547
|
+
const ownMsg = msg === undefined;
|
|
548
|
+
const m = ownMsg ? randomBytes(msgLen) : (msg as TArg<Uint8Array>);
|
|
549
|
+
let kr: Uint8Array | undefined;
|
|
550
|
+
try {
|
|
551
|
+
abytes(m, msgLen, 'message');
|
|
552
|
+
kr = HASH512.create().update(m).update(publicKeyHash).digest();
|
|
553
|
+
const cipherText = cached.encrypt(m, kr.subarray(32, 64));
|
|
554
|
+
return {
|
|
555
|
+
cipherText: cipherText as TRet<Uint8Array>,
|
|
556
|
+
sharedSecret: kr.subarray(0, 32) as TRet<Uint8Array>,
|
|
557
|
+
};
|
|
558
|
+
} finally {
|
|
559
|
+
if (kr !== undefined) cleanBytes(kr.subarray(32));
|
|
560
|
+
if (ownMsg) cleanBytes(m);
|
|
561
|
+
}
|
|
521
562
|
},
|
|
522
563
|
decapsulate: (
|
|
523
564
|
cipherText: TArg<Uint8Array>,
|
package/src/slh-dsa.ts
CHANGED
|
@@ -53,6 +53,13 @@ import {
|
|
|
53
53
|
type VerOpts,
|
|
54
54
|
} from './utils.ts';
|
|
55
55
|
|
|
56
|
+
// Keys the internal SLH-DSA surface accepts. `context` is deliberately absent: the public
|
|
57
|
+
// wrappers consume it when they format M' and must not forward it, because a key that is
|
|
58
|
+
// accepted and then never read is the same silent downgrade this validation exists to prevent.
|
|
59
|
+
// `extraEntropy` is signing-only, so verification (which takes no options of its own) has none.
|
|
60
|
+
const INTERNAL_SIG_OPT_KEYS = /* @__PURE__ */ Object.freeze(['extraEntropy'] as const);
|
|
61
|
+
const INTERNAL_VER_OPT_KEYS = /* @__PURE__ */ Object.freeze([] as const);
|
|
62
|
+
|
|
56
63
|
/**
|
|
57
64
|
* * N: Security parameter (in bytes). W: Winternitz parameter
|
|
58
65
|
* * H: Hypertree height. D: Hypertree layers
|
|
@@ -83,6 +90,8 @@ export type SphincsHashOpts = {
|
|
|
83
90
|
getContext: GetContext;
|
|
84
91
|
};
|
|
85
92
|
|
|
93
|
+
type InternalSphincsHashOpts = SphincsHashOpts & { isCompressed: boolean };
|
|
94
|
+
|
|
86
95
|
/** Winternitz signature params. */
|
|
87
96
|
/**
|
|
88
97
|
* Built-in SLH-DSA Table 2 subset keyed by strength/profile.
|
|
@@ -218,8 +227,8 @@ export type SphincsSigner = Signer & {
|
|
|
218
227
|
* and `getPublicKey(secretKey)` only extracts the embedded public key
|
|
219
228
|
* instead of recomputing `PK.root`.
|
|
220
229
|
*/
|
|
221
|
-
function gen(opts: SphincsOpts, hashOpts_: TArg<
|
|
222
|
-
const hashOpts = hashOpts_ as
|
|
230
|
+
function gen(opts: SphincsOpts, hashOpts_: TArg<InternalSphincsHashOpts>): TRet<SphincsSigner> {
|
|
231
|
+
const hashOpts = hashOpts_ as InternalSphincsHashOpts;
|
|
223
232
|
const { N, W, H, D, K, A, securityLevel: securityLevel } = opts;
|
|
224
233
|
const getContext = hashOpts.getContext(opts);
|
|
225
234
|
if (W !== 16) throw new Error('Unsupported Winternitz parameter');
|
|
@@ -270,8 +279,18 @@ function gen(opts: SphincsOpts, hashOpts_: TArg<SphincsHashOpts>): TRet<SphincsS
|
|
|
270
279
|
}>,
|
|
271
280
|
addr: TArg<ADRS> = new Uint8Array(ADDR_BYTES)
|
|
272
281
|
) => {
|
|
273
|
-
|
|
274
|
-
|
|
282
|
+
// These objects are created in hot internal loops, so avoid cloning them. Read only own fields:
|
|
283
|
+
// absent address words must stay absent even if Object.prototype was polluted.
|
|
284
|
+
const type = Object.hasOwn(opts, 'type') ? opts.type : undefined;
|
|
285
|
+
const height = Object.hasOwn(opts, 'height') ? opts.height : undefined;
|
|
286
|
+
const tree = Object.hasOwn(opts, 'tree') ? opts.tree : undefined;
|
|
287
|
+
const layer = Object.hasOwn(opts, 'layer') ? opts.layer : undefined;
|
|
288
|
+
const index = Object.hasOwn(opts, 'index') ? opts.index : undefined;
|
|
289
|
+
const chain = Object.hasOwn(opts, 'chain') ? opts.chain : undefined;
|
|
290
|
+
const hash = Object.hasOwn(opts, 'hash') ? opts.hash : undefined;
|
|
291
|
+
const keypair = Object.hasOwn(opts, 'keypair') ? opts.keypair : undefined;
|
|
292
|
+
const subtreeAddr = Object.hasOwn(opts, 'subtreeAddr') ? opts.subtreeAddr : undefined;
|
|
293
|
+
const keypairAddr = Object.hasOwn(opts, 'keypairAddr') ? opts.keypairAddr : undefined;
|
|
275
294
|
|
|
276
295
|
if (height !== undefined) addr[OFFSET_CHAIN_ADDR] = height;
|
|
277
296
|
if (layer !== undefined) addr[OFFSET_LAYER] = layer;
|
|
@@ -547,7 +566,7 @@ function gen(opts: SphincsOpts, hashOpts_: TArg<SphincsHashOpts>): TRet<SphincsS
|
|
|
547
566
|
return Uint8Array.from(pk) as TRet<Uint8Array>;
|
|
548
567
|
},
|
|
549
568
|
sign: (msg: TArg<Uint8Array>, sk: TArg<Uint8Array>, opts: TArg<SigOpts> = {}) => {
|
|
550
|
-
validateSigOpts(opts);
|
|
569
|
+
opts = validateSigOpts(opts, INTERNAL_SIG_OPT_KEYS);
|
|
551
570
|
let { extraEntropy: random } = opts;
|
|
552
571
|
const [skSeed, skPRF, pk] = secretCoder.decode(sk); // todo: fix
|
|
553
572
|
const [pkSeed, _] = publicCoder.decode(pk);
|
|
@@ -623,7 +642,16 @@ function gen(opts: SphincsOpts, hashOpts_: TArg<SphincsHashOpts>): TRet<SphincsS
|
|
|
623
642
|
cleanBytes(R, random, treeAddr, wotsAddr, forsLeaf, forsTreeAddr, indices, roots);
|
|
624
643
|
return SIG as TRet<Uint8Array>;
|
|
625
644
|
},
|
|
626
|
-
verify: (
|
|
645
|
+
verify: (
|
|
646
|
+
sig: TArg<Uint8Array>,
|
|
647
|
+
msg: TArg<Uint8Array>,
|
|
648
|
+
publicKey: TArg<Uint8Array>,
|
|
649
|
+
opts: TArg<VerOpts> = {}
|
|
650
|
+
) => {
|
|
651
|
+
// The internal verify reads no options; reject any so a stray key (e.g. a caller
|
|
652
|
+
// mistaking this for the public verify and passing `context`) is reported rather than
|
|
653
|
+
// silently swallowed by this function's arity.
|
|
654
|
+
validateVerOpts(opts, INTERNAL_VER_OPT_KEYS);
|
|
627
655
|
const [pkSeed, pubRoot] = publicCoder.decode(publicKey);
|
|
628
656
|
const pk = publicKey;
|
|
629
657
|
// FIPS 205 Algorithm 20 step 1: wrong-length signatures return false instead of throwing
|
|
@@ -700,9 +728,11 @@ function gen(opts: SphincsOpts, hashOpts_: TArg<SphincsHashOpts>): TRet<SphincsS
|
|
|
700
728
|
keygen: internal.keygen,
|
|
701
729
|
getPublicKey: internal.getPublicKey,
|
|
702
730
|
sign: (msg: TArg<Uint8Array>, secretKey: TArg<Uint8Array>, opts: TArg<SigOpts> = {}) => {
|
|
703
|
-
validateSigOpts(opts);
|
|
731
|
+
opts = validateSigOpts(opts);
|
|
704
732
|
const M = getMessage(msg, opts.context);
|
|
705
|
-
|
|
733
|
+
// `context` is consumed by getMessage() above; forwarding it would make the internal
|
|
734
|
+
// surface accept a key it never reads.
|
|
735
|
+
const res = internal.sign(M, secretKey, { extraEntropy: opts.extraEntropy });
|
|
706
736
|
cleanBytes(M);
|
|
707
737
|
return res as TRet<Uint8Array>;
|
|
708
738
|
},
|
|
@@ -712,7 +742,7 @@ function gen(opts: SphincsOpts, hashOpts_: TArg<SphincsHashOpts>): TRet<SphincsS
|
|
|
712
742
|
publicKey: TArg<Uint8Array>,
|
|
713
743
|
opts: TArg<VerOpts> = {}
|
|
714
744
|
) => {
|
|
715
|
-
validateVerOpts(opts);
|
|
745
|
+
opts = validateVerOpts(opts);
|
|
716
746
|
return internal.verify(sig, getMessage(msg, opts.context), publicKey);
|
|
717
747
|
},
|
|
718
748
|
prehash: (hash: TArg<CHash>): TRet<Signer> => {
|
|
@@ -724,9 +754,10 @@ function gen(opts: SphincsOpts, hashOpts_: TArg<SphincsHashOpts>): TRet<SphincsS
|
|
|
724
754
|
keygen: internal.keygen,
|
|
725
755
|
getPublicKey: internal.getPublicKey,
|
|
726
756
|
sign: (msg: TArg<Uint8Array>, secretKey: TArg<Uint8Array>, opts: TArg<SigOpts> = {}) => {
|
|
727
|
-
validateSigOpts(opts);
|
|
757
|
+
opts = validateSigOpts(opts);
|
|
728
758
|
const M = getMessagePrehash(rawHash, msg, opts.context);
|
|
729
|
-
|
|
759
|
+
// As above: getMessagePrehash() consumes `context`, so it must not travel further.
|
|
760
|
+
const res = internal.sign(M, secretKey, { extraEntropy: opts.extraEntropy });
|
|
730
761
|
cleanBytes(M);
|
|
731
762
|
return res as TRet<Uint8Array>;
|
|
732
763
|
},
|
|
@@ -736,7 +767,7 @@ function gen(opts: SphincsOpts, hashOpts_: TArg<SphincsHashOpts>): TRet<SphincsS
|
|
|
736
767
|
publicKey: TArg<Uint8Array>,
|
|
737
768
|
opts: TArg<VerOpts> = {}
|
|
738
769
|
) => {
|
|
739
|
-
validateVerOpts(opts);
|
|
770
|
+
opts = validateVerOpts(opts);
|
|
740
771
|
return internal.verify(sig, getMessagePrehash(rawHash, msg, opts.context), publicKey);
|
|
741
772
|
},
|
|
742
773
|
});
|
|
@@ -812,7 +843,7 @@ const genShake =
|
|
|
812
843
|
} as TRet<Context>;
|
|
813
844
|
};
|
|
814
845
|
|
|
815
|
-
const SHAKE_SIMPLE = /* @__PURE__ */ (() => ({ getContext: genShake() }))();
|
|
846
|
+
const SHAKE_SIMPLE = /* @__PURE__ */ (() => ({ isCompressed: false, getContext: genShake() }))();
|
|
816
847
|
|
|
817
848
|
/**
|
|
818
849
|
* SLH-DSA-SHAKE-128f: Table 2 row `n=16, h=66, d=22, h'=3, a=6, k=33, lg w=4, m=34`;
|
package/src/utils.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
abytes as abytes_,
|
|
11
11
|
ahash as ahash_,
|
|
12
12
|
anumber,
|
|
13
|
+
bytesToHex,
|
|
13
14
|
concatBytes,
|
|
14
15
|
isBytes,
|
|
15
16
|
isLE,
|
|
@@ -215,9 +216,10 @@ export function equalBytes(a: TArg<Uint8Array>, b: TArg<Uint8Array>): boolean {
|
|
|
215
216
|
* ```
|
|
216
217
|
*/
|
|
217
218
|
export function copyBytes(bytes: TArg<Uint8Array>): TRet<Uint8Array> {
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
|
|
219
|
+
// The typed-array constructor copies a typed-array source through its internal byte storage.
|
|
220
|
+
// Unlike `Uint8Array.from`, it does not invoke a subclass-overridden iterator. Keep the explicit
|
|
221
|
+
// validation because the constructor itself would also accept arrays and other typed arrays.
|
|
222
|
+
return new Uint8Array(abytes(bytes)) as TRet<Uint8Array>;
|
|
221
223
|
}
|
|
222
224
|
|
|
223
225
|
/**
|
|
@@ -315,6 +317,66 @@ export function validateOpts(opts: object): void {
|
|
|
315
317
|
// Arrays silently passed here before, but these call sites expect named option-bag fields.
|
|
316
318
|
if (isBytes(opts)) throw new TypeError('"opts" expected object, got Uint8Array');
|
|
317
319
|
aobject(opts, 'opts');
|
|
320
|
+
const proto = Object.getPrototypeOf(opts);
|
|
321
|
+
// Options are security parameters, not general class instances. Restricting the bag to own
|
|
322
|
+
// properties prevents values injected through Object.prototype (or a custom shared prototype)
|
|
323
|
+
// from silently changing signing behavior. Null-prototype records remain supported.
|
|
324
|
+
if (proto !== null && proto !== Object.prototype)
|
|
325
|
+
throw new TypeError('"opts" expected a plain object');
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Frozen because they are exported: an unfrozen array export lets anything in the
|
|
329
|
+
// process push a key onto the accepted set and silently re-open exactly the hole this
|
|
330
|
+
// validation closes.
|
|
331
|
+
/** Keys accepted by `verify`. */
|
|
332
|
+
export const VER_OPT_KEYS: readonly ['context'] = /* @__PURE__ */ Object.freeze([
|
|
333
|
+
'context',
|
|
334
|
+
] as const);
|
|
335
|
+
/** Keys accepted by `sign`. */
|
|
336
|
+
export const SIG_OPT_KEYS: readonly ['context', 'extraEntropy'] = /* @__PURE__ */ Object.freeze([
|
|
337
|
+
'context',
|
|
338
|
+
'extraEntropy',
|
|
339
|
+
] as const);
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Rejects option keys the caller did not mean to set.
|
|
343
|
+
*
|
|
344
|
+
* Validating the types of known keys while ignoring unknown ones makes a typo
|
|
345
|
+
* indistinguishable from an omission, and for these options an omission is a
|
|
346
|
+
* security downgrade rather than a no-op: `{ ctx }` instead of `{ context }` signs
|
|
347
|
+
* with no domain separation, succeeds, and verifies for anyone who also supplies
|
|
348
|
+
* none. Nothing at any layer reports it. TypeScript catches this through excess
|
|
349
|
+
* property checks, so the exposure is JavaScript callers specifically.
|
|
350
|
+
*
|
|
351
|
+
* @param opts - Options object to check.
|
|
352
|
+
* @param allowed - The keys this call site accepts.
|
|
353
|
+
* Returns a frozen null-prototype snapshot so later reads cannot fall through to a polluted
|
|
354
|
+
* prototype. Like `checkOpts()` in noble-hashes, only enumerable own properties are copied.
|
|
355
|
+
* @throws If any other copied key is present or the bag has a custom prototype. {@link TypeError}
|
|
356
|
+
* @returns Sanitized snapshot of the enumerable own options.
|
|
357
|
+
* @example
|
|
358
|
+
* Accept a known option key. A key the list does not name, such as `ctx`, throws instead.
|
|
359
|
+
* ```ts
|
|
360
|
+
* import { checkOptKeys } from '@noble/post-quantum/utils.js';
|
|
361
|
+
* checkOptKeys({ context: new Uint8Array() }, ['context']);
|
|
362
|
+
* ```
|
|
363
|
+
*/
|
|
364
|
+
export function checkOptKeys<T extends object>(opts: T, allowed: readonly string[]): T {
|
|
365
|
+
validateOpts(opts);
|
|
366
|
+
// Snapshot once before validation: Object.assign follows the same own-enumerable option-bag
|
|
367
|
+
// semantics as noble-hashes, while the null prototype keeps omitted fields immune to pollution.
|
|
368
|
+
const normalized = Object.assign(Object.create(null), opts) as Record<string, unknown>;
|
|
369
|
+
for (const [k, v] of Object.entries(normalized)) {
|
|
370
|
+
// `undefined` means unset everywhere else in these validators, and building an options bag by
|
|
371
|
+
// spread is a normal way to reach these calls, so present-but-undefined stays equivalent to
|
|
372
|
+
// omission.
|
|
373
|
+
if (v === undefined) continue;
|
|
374
|
+
if (!allowed.includes(k))
|
|
375
|
+
throw new TypeError(
|
|
376
|
+
'unexpected option "' + String(k) + '"; expected one of: ' + allowed.join(', ')
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
return Object.freeze(normalized) as T;
|
|
318
380
|
}
|
|
319
381
|
|
|
320
382
|
/**
|
|
@@ -322,16 +384,23 @@ export function validateOpts(opts: object): void {
|
|
|
322
384
|
* `context` itself is validated with `abytes(...)`, and individual algorithms may narrow support
|
|
323
385
|
* further after this shared plain-object gate.
|
|
324
386
|
* @param opts - Verification options. See {@link VerOpts}.
|
|
387
|
+
* @param allowed - Keys this call site accepts. Defaults to {@link VER_OPT_KEYS}; surfaces that
|
|
388
|
+
* take extra keys, or take fewer, pass their own list.
|
|
325
389
|
* @throws On wrong argument types. {@link TypeError}
|
|
390
|
+
* @returns Frozen null-prototype snapshot of the validated options.
|
|
326
391
|
* @example
|
|
327
392
|
* Validate common verification options.
|
|
328
393
|
* ```ts
|
|
329
394
|
* validateVerOpts({ context: new Uint8Array([1]) });
|
|
330
395
|
* ```
|
|
331
396
|
*/
|
|
332
|
-
export function validateVerOpts
|
|
333
|
-
|
|
334
|
-
|
|
397
|
+
export function validateVerOpts<T extends TArg<VerOpts>>(
|
|
398
|
+
opts: T,
|
|
399
|
+
allowed: readonly string[] = VER_OPT_KEYS
|
|
400
|
+
): T {
|
|
401
|
+
const normalized = checkOptKeys(opts, allowed);
|
|
402
|
+
if (normalized.context !== undefined) abytes(normalized.context, undefined, 'opts.context');
|
|
403
|
+
return normalized;
|
|
335
404
|
}
|
|
336
405
|
|
|
337
406
|
/**
|
|
@@ -339,17 +408,25 @@ export function validateVerOpts(opts: TArg<VerOpts>): void {
|
|
|
339
408
|
* `extraEntropy` is validated with `abytes(...)`; exact lengths and extra algorithm-specific
|
|
340
409
|
* restrictions are enforced later by callers.
|
|
341
410
|
* @param opts - Signing options. See {@link SigOpts}.
|
|
411
|
+
* @param allowed - Keys this call site accepts. Defaults to {@link SIG_OPT_KEYS}; surfaces that
|
|
412
|
+
* take extra keys, or take fewer, pass their own list.
|
|
342
413
|
* @throws On wrong argument types. {@link TypeError}
|
|
414
|
+
* @returns Frozen null-prototype snapshot of the validated options.
|
|
343
415
|
* @example
|
|
344
416
|
* Validate common signing options.
|
|
345
417
|
* ```ts
|
|
346
418
|
* validateSigOpts({ extraEntropy: new Uint8Array([1]) });
|
|
347
419
|
* ```
|
|
348
420
|
*/
|
|
349
|
-
export function validateSigOpts
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
421
|
+
export function validateSigOpts<T extends TArg<SigOpts>>(
|
|
422
|
+
opts: T,
|
|
423
|
+
allowed: readonly string[] = SIG_OPT_KEYS
|
|
424
|
+
): T {
|
|
425
|
+
const normalized = checkOptKeys(opts, allowed);
|
|
426
|
+
if (normalized.context !== undefined) abytes(normalized.context, undefined, 'opts.context');
|
|
427
|
+
if (normalized.extraEntropy !== false && normalized.extraEntropy !== undefined)
|
|
428
|
+
abytes(normalized.extraEntropy, undefined, 'opts.extraEntropy');
|
|
429
|
+
return normalized;
|
|
353
430
|
}
|
|
354
431
|
|
|
355
432
|
/** Generic signature interface with key generation, signing, and verification. */
|
|
@@ -617,6 +694,20 @@ export function getMessage(msg: TArg<Uint8Array>, ctx: TArg<Uint8Array> = EMPTY)
|
|
|
617
694
|
// 06 09 60 86 48 01 65 03 04 02
|
|
618
695
|
const oidNistP = /* @__PURE__ */ Uint8Array.from([6, 9, 0x60, 0x86, 0x48, 1, 0x65, 3, 4, 2]);
|
|
619
696
|
|
|
697
|
+
/**
|
|
698
|
+
* Output length, in bytes, that each XOF OID under this arc denotes.
|
|
699
|
+
*
|
|
700
|
+
* Unlike a fixed hash, an XOF's OID is a promise about the digest length: RFC 8702
|
|
701
|
+
* defines id-shake128 as SHAKE128 with 256-bit output and id-shake256 as SHAKE256 with
|
|
702
|
+
* 512-bit output, and FIPS 204 / FIPS 205 use exactly those pairings for pre-hash. Both
|
|
703
|
+
* bare noble-hashes defaults are half these values, so neither can be signed under its
|
|
704
|
+
* own OID.
|
|
705
|
+
*/
|
|
706
|
+
const XOF_OID_OUTPUT_LEN: Record<string, number> = /* @__PURE__ */ (() => ({
|
|
707
|
+
'060960864801650304020b': 32, // id-shake128, SHAKE128(M, 256)
|
|
708
|
+
'060960864801650304020c': 64, // id-shake256, SHAKE256(M, 512)
|
|
709
|
+
}))();
|
|
710
|
+
|
|
620
711
|
/**
|
|
621
712
|
* Validates that a hash exposes a NIST hash OID and enough collision resistance.
|
|
622
713
|
* Current accepted surface is broader than the FIPS algorithm tables: any hash/XOF under the NIST
|
|
@@ -646,6 +737,20 @@ export function checkHash(hash: CHash, requiredStrength: number = 0): void {
|
|
|
646
737
|
// FIPS 204 / FIPS 205 require both collision and second-preimage strength; for approved NIST
|
|
647
738
|
// hashes/XOFs under this OID subtree, the collision bound from the configured digest length is
|
|
648
739
|
// the tighter runtime check, so enforce that lower bound here.
|
|
740
|
+
// XOFs under this arc are identified by an OID that fixes their output length:
|
|
741
|
+
// FIPS 204 §5.4.1 (SHAKE128) and FIPS 205 §10.2.2 (both SHAKEs), matching RFC 8702, pair
|
|
742
|
+
// id-shake128 with SHAKE128(M, 256) and id-shake256 with SHAKE256(M, 512). getMessagePrehash embeds
|
|
743
|
+
// hash.oid beside hash(msg), so a shorter digest signs an M' that claims a length
|
|
744
|
+
// it does not have: noble-hashes' bare shake256 defaults to 32 bytes and cleared
|
|
745
|
+
// the collision bound at the 128-bit level, producing signatures a conformant
|
|
746
|
+
// verifier rejects because it recomputes 512 bits. Check the length the OID
|
|
747
|
+
// denotes rather than the generic bound.
|
|
748
|
+
const xofLen = XOF_OID_OUTPUT_LEN[bytesToHex(oid as Uint8Array)];
|
|
749
|
+
if (xofLen !== undefined && hash.outputLen !== xofLen) {
|
|
750
|
+
throw new Error(
|
|
751
|
+
'Pre-hash XOF output length must be ' + xofLen + ' bytes for this OID, got: ' + hash.outputLen
|
|
752
|
+
);
|
|
753
|
+
}
|
|
649
754
|
const collisionResistance = (hash.outputLen * 8) / 2;
|
|
650
755
|
if (requiredStrength > collisionResistance) {
|
|
651
756
|
throw new Error(
|