@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/README.md
CHANGED
|
@@ -79,7 +79,7 @@ import {
|
|
|
79
79
|
- [ML-DSA / Dilithium](#ml-dsa--dilithium-signatures)
|
|
80
80
|
- [SLH-DSA / SPHINCS+](#slh-dsa--sphincs-signatures)
|
|
81
81
|
- [Falcon](#falcon-signatures)
|
|
82
|
-
- [hybrid:
|
|
82
|
+
- [hybrid: X-Wing, KitchenSink and others](#hybrid-x-wing-kitchensink-and-others)
|
|
83
83
|
- [What should I use?](#what-should-i-use)
|
|
84
84
|
- [Security](#security)
|
|
85
85
|
- [Contributing & testing](#contributing--testing)
|
|
@@ -123,6 +123,25 @@ Old, incompatible version (Kyber) is not provided. Open an issue if you need it.
|
|
|
123
123
|
> `decapsulate` will simply return a different shared secret.
|
|
124
124
|
> ML-KEM is also probabilistic and relies on quality of CSPRNG.
|
|
125
125
|
|
|
126
|
+
#### webcrypto: friendly wrapper
|
|
127
|
+
|
|
128
|
+
WebCrypto-backed ML-KEM and `ml_kem768_x25519` wrappers are also available. Their methods are async
|
|
129
|
+
and require a runtime that implements the corresponding experimental WebCrypto API.
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
import { ml_kem768 } from '@noble/post-quantum/webcrypto.js';
|
|
133
|
+
|
|
134
|
+
if (await ml_kem768.isSupported()) {
|
|
135
|
+
const aliceKeys = await ml_kem768.keygen();
|
|
136
|
+
const { cipherText, sharedSecret: bobShared } = await ml_kem768.encapsulate(aliceKeys.publicKey);
|
|
137
|
+
const aliceShared = await ml_kem768.decapsulate(cipherText, aliceKeys.secretKey);
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The ML-KEM wrappers serialize private keys as 64-byte `raw-seed` values; the X25519 hybrid uses a
|
|
142
|
+
32-byte seed. They can be passed to the corresponding synchronous implementation's `keygen(seed)`,
|
|
143
|
+
but are not expanded decapsulation keys.
|
|
144
|
+
|
|
126
145
|
### ML-DSA / Dilithium signatures
|
|
127
146
|
|
|
128
147
|
```ts
|
|
@@ -157,9 +176,17 @@ const isValidPre = hml.verify(sigPre, msg, keys.publicKey);
|
|
|
157
176
|
- `context`: domain-separation byte string, up to 255 bytes; must match between `sign` and `verify`
|
|
158
177
|
- `extraEntropy`: hedged-signing randomness. Default is 32 random bytes;
|
|
159
178
|
`false` produces deterministic signatures; custom 32-byte value is also allowed
|
|
160
|
-
- `externalMu`: treat `msg` as the precomputed 64-byte message representative µ
|
|
161
179
|
- `prehash(hash)`: pre-hash variant (HashML-DSA) from FIPS-204
|
|
162
180
|
|
|
181
|
+
Unknown option keys are rejected rather than ignored, so a misspelling such as
|
|
182
|
+
`{ ctx }` fails loudly instead of silently signing with no domain separation.
|
|
183
|
+
|
|
184
|
+
`externalMu`, which treats `msg` as the precomputed 64-byte message representative
|
|
185
|
+
µ, is available on `ml_dsa*.internal.sign` / `internal.verify` only. The public
|
|
186
|
+
wrappers reject it: `sign` formats `M'` before the 64-byte check so it could never
|
|
187
|
+
accept a µ, and `verify` did not forward it, returning `false` for a valid
|
|
188
|
+
external-mu signature.
|
|
189
|
+
|
|
163
190
|
### SLH-DSA / SPHINCS+ signatures
|
|
164
191
|
|
|
165
192
|
```ts
|
|
@@ -218,7 +245,27 @@ Lattice-based digital signature algorithm, submitted to NIST PQC Round 3 ([websi
|
|
|
218
245
|
- `falcon512padded`, `falcon1024padded`: fixed-length detached signatures
|
|
219
246
|
- `attached.seal(...)` / `attached.open(...)`: attached-signature API for Round 3 vectors and interop
|
|
220
247
|
|
|
221
|
-
|
|
248
|
+
> [!WARNING]
|
|
249
|
+
> Falcon signing is randomized by design. Leave signing options unset in production so every
|
|
250
|
+
> signature receives a fresh 40-byte public nonce and a fresh 48-byte sampler seed from the system
|
|
251
|
+
> CSPRNG. Falcon's `extraEntropy` option does not have the hedged semantics used by ML-DSA and
|
|
252
|
+
> SLH-DSA:
|
|
253
|
+
>
|
|
254
|
+
> - `extraEntropy: false` seeds an AES-CTR-DRBG with 48 zero bytes. It makes signatures
|
|
255
|
+
> deterministic for a fixed key and message, and reuses the same nonce and initial random stream
|
|
256
|
+
> across different messages. This is outside the Falcon Round 3 randomized-hash design.
|
|
257
|
+
> - A 48-byte `extraEntropy` value replaces system randomness; it is not mixed with fresh entropy.
|
|
258
|
+
> Reusing a value therefore reuses the signing stream.
|
|
259
|
+
> - The raw `random` callback overrides `extraEntropy` and supplies both the nonce and sampler seed.
|
|
260
|
+
> It exists for test-vector reproduction and should not be used as a production randomness hook.
|
|
261
|
+
>
|
|
262
|
+
> In particular, do not copy ML-DSA examples that use `extraEntropy: false` into Falcon code.
|
|
263
|
+
|
|
264
|
+
`attached.open(...)` throws when verification fails and returns a fresh copy of the embedded
|
|
265
|
+
message when it succeeds. The result does not alias the attached signature or public-key buffers.
|
|
266
|
+
Detached `verify(...)` returns `false` for an invalid signature.
|
|
267
|
+
|
|
268
|
+
### hybrid: X-Wing, KitchenSink and others
|
|
222
269
|
|
|
223
270
|
```js
|
|
224
271
|
import {
|
|
@@ -230,19 +277,32 @@ import {
|
|
|
230
277
|
|
|
231
278
|
The hybrid submodule combines post-quantum algorithms with elliptic curve cryptography:
|
|
232
279
|
|
|
233
|
-
- `ml_kem768_x25519`: ML-KEM-768 + X25519
|
|
234
|
-
|
|
235
|
-
- `
|
|
280
|
+
- `ml_kem768_x25519`: ML-KEM-768 + X25519, implementing X-Wing under the descriptive
|
|
281
|
+
`ml_kem768_x25519` export name. There is no separate `XWing` alias.
|
|
282
|
+
- `ml_kem768_p256`: ML-KEM-768 + P-256 using the current CG framework construction
|
|
283
|
+
- `ml_kem1024_p384`: ML-KEM-1024 + P-384 using the current CG framework construction
|
|
236
284
|
- `KitchenSink_ml_kem768_x25519`: ML-KEM-768 + X25519 with HKDF-SHA256 combiner
|
|
237
|
-
- `QSF_ml_kem768_p256`:
|
|
238
|
-
|
|
285
|
+
- `QSF_ml_kem768_p256`, `QSF_ml_kem1024_p384`: legacy compatibility presets for the older
|
|
286
|
+
QSF/C2PRI naming and labels. New code should use `ml_kem768_p256` and `ml_kem1024_p384`.
|
|
287
|
+
|
|
288
|
+
> **Security note:** `_ecdhKem(curve)` is an internal raw-ECDH component adapter, not a standalone
|
|
289
|
+
> IND-CCA-secure KEM. It has no KDF and does not bind the encapsulation or recipient public key, so
|
|
290
|
+
> different accepted point encodings can derive the same bytes. Use it only within a specified
|
|
291
|
+
> combiner that performs that binding, or use a standardized DHKEM. The built-in hybrid presets
|
|
292
|
+
> retain their specified combiners and test-vector-compatible behavior.
|
|
293
|
+
|
|
294
|
+
The current `ml_kem*` presets are tested against these work-in-progress specifications:
|
|
239
295
|
|
|
240
|
-
|
|
296
|
+
- [irtf-cfrg-hybrid-kems-12](https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hybrid-kems-12)
|
|
297
|
+
- [irtf-cfrg-concrete-hybrid-kems-03](https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-concrete-hybrid-kems-03)
|
|
298
|
+
- [connolly-cfrg-xwing-kem-10](https://datatracker.ietf.org/doc/html/draft-connolly-cfrg-xwing-kem-10)
|
|
241
299
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
300
|
+
`QSF(...)` is the legacy API name for the construction now called the C2PRI combiner. It derives
|
|
301
|
+
the final secret from `ssPQ || ssT || ctT || ekT || label`; omitting the PQ ciphertext and
|
|
302
|
+
encapsulation key is intentional and relies on the PQ KEM's C2PRI property. The `QSF_*` presets
|
|
303
|
+
retain older draft labels and vectors for compatibility, so they do not implement the current
|
|
304
|
+
concrete preset encodings. They are also unrelated to the separate universal-combiner example in
|
|
305
|
+
NIST SP 800-227.
|
|
246
306
|
|
|
247
307
|
### What should I use?
|
|
248
308
|
|
|
@@ -282,9 +342,23 @@ If you see anything unusual: investigate and report.
|
|
|
282
342
|
|
|
283
343
|
### Constant-timeness
|
|
284
344
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
345
|
+
This pure JavaScript implementation does not claim constant-time execution. JavaScript engines,
|
|
346
|
+
JIT compilers, garbage collection, floating-point operations and `bigint` arithmetic do not offer
|
|
347
|
+
the execution guarantees needed for a formal constant-time claim.
|
|
348
|
+
|
|
349
|
+
- ML-DSA signing uses rejection loops, early-exit norm checks and conditional arithmetic whose
|
|
350
|
+
execution depends on secret-key and per-signature state. Fresh randomized signing is the default,
|
|
351
|
+
but it does not turn the implementation into a constant-time one.
|
|
352
|
+
- Falcon signing uses data-dependent Gaussian and rejection sampling, floating-point operations,
|
|
353
|
+
and `bigint` paths. Its timing and microarchitectural side-channel posture is materially weaker
|
|
354
|
+
than a hardened native implementation. Deterministic or repeated signing randomness can make
|
|
355
|
+
observations easier to correlate and should be avoided.
|
|
356
|
+
- These limitations matter most when an attacker can measure signing closely, such as hostile
|
|
357
|
+
co-tenancy, shared hardware, or a high-resolution local timing oracle. Use an isolated execution
|
|
358
|
+
environment or a reviewed native/constant-time backend when that is part of the threat model.
|
|
359
|
+
|
|
360
|
+
We actively research how to improve this property for post-quantum algorithms in JS. Even hardware
|
|
361
|
+
ML-KEM implementations require careful side-channel engineering and [have had practical attacks](https://eprint.iacr.org/2023/1084).
|
|
288
362
|
|
|
289
363
|
### Supply chain security
|
|
290
364
|
|
package/_crystals.js
CHANGED
|
@@ -31,7 +31,7 @@ export const genCrystals = (opts) => {
|
|
|
31
31
|
// Normalize JS `%` into the canonical Z_m representative `[0, modulo-1]` expected by
|
|
32
32
|
// FIPS 203 §2.3 / FIPS 204 §2.3 before downstream mod-q arithmetic.
|
|
33
33
|
const mod = (a, modulo = Q) => {
|
|
34
|
-
const result = a % modulo | 0;
|
|
34
|
+
const result = (a % modulo) | 0;
|
|
35
35
|
return (result >= 0 ? result | 0 : (modulo + result) | 0) | 0;
|
|
36
36
|
};
|
|
37
37
|
// FIPS 204 §7.4 uses the centered `mod ±` representative for low bits, keeping the
|
package/falcon.d.ts
CHANGED
|
@@ -22,7 +22,7 @@ export type FalconAttached = CryptoKeys & {
|
|
|
22
22
|
* @param sig Attached Falcon signature bytes.
|
|
23
23
|
* @param publicKey Falcon public key bytes.
|
|
24
24
|
* @param opts Optional verification options.
|
|
25
|
-
* @returns
|
|
25
|
+
* @returns Fresh message bytes that do not alias either input when the signature is valid.
|
|
26
26
|
*/
|
|
27
27
|
open(sig: Uint8Array, publicKey: Uint8Array, opts?: VerOpts): Uint8Array;
|
|
28
28
|
};
|
package/falcon.js
CHANGED
|
@@ -12,7 +12,7 @@ import { bytesToNumberLE, numberToHexUnpadded } from '@noble/curves/utils.js';
|
|
|
12
12
|
import { shake256 } from '@noble/hashes/sha3.js';
|
|
13
13
|
import { abytes, bytesToHex, createView, hexToBytes, randomBytes, swap32IfBE, u32, u8, } from '@noble/hashes/utils.js';
|
|
14
14
|
import { genCrystals } from "./_crystals.js";
|
|
15
|
-
import { baswap64If, cleanBytes, getMask, splitCoder, validateSigOpts, validateVerOpts, } from "./utils.js";
|
|
15
|
+
import { baswap64If, cleanBytes, copyBytes, getMask, splitCoder, validateSigOpts, validateVerOpts, SIG_OPT_KEYS, } from "./utils.js";
|
|
16
16
|
/*
|
|
17
17
|
FIPS-206 would likely improve the situation with spec.
|
|
18
18
|
|
|
@@ -230,8 +230,13 @@ const compCoder = (n) => {
|
|
|
230
230
|
const sign = readBits(1);
|
|
231
231
|
const low = readBits(7);
|
|
232
232
|
let high = 0;
|
|
233
|
-
for
|
|
234
|
-
|
|
233
|
+
// Reference comp_decode adds 128 for each unary zero and rejects immediately above 2047.
|
|
234
|
+
// Waiting for the terminating one first lets an invalid coefficient scan the entire input.
|
|
235
|
+
while (!readBits(1)) {
|
|
236
|
+
high++;
|
|
237
|
+
if (high > LIMIT >>> 7)
|
|
238
|
+
throw new Error(`limit: ${low | (high << 7)} > ${LIMIT}`);
|
|
239
|
+
}
|
|
235
240
|
const v = low | (high << 7);
|
|
236
241
|
if (sign && v === 0)
|
|
237
242
|
throw new Error('negative zero encoding');
|
|
@@ -510,6 +515,9 @@ const SIGMA_MIN = /* @__PURE__ */ Object.freeze([
|
|
|
510
515
|
f64b(BigInt('4608433670533905013')),
|
|
511
516
|
f64b(BigInt('4608525754002622308')),
|
|
512
517
|
]);
|
|
518
|
+
// Upper end of the SamplerZ proof interval from Falcon section 3.9.1. The per-leaf sigma is
|
|
519
|
+
// derived from the reconstructed private basis, so imported keys must be checked against it.
|
|
520
|
+
const SIGMA_MAX = 1.8205;
|
|
513
521
|
// Falcon Table 3.1 RCDT values for chi, split into 24-bit limbs; storage is [high, mid, low],
|
|
514
522
|
// so gaussian0() intentionally compares them against v0, v1, v2 in reverse order. The final
|
|
515
523
|
// RCDT[18] = 0 row is omitted because the algorithm iterates only over i = 0..17.
|
|
@@ -1111,6 +1119,51 @@ function getFloatPoly(logn) {
|
|
|
1111
1119
|
},
|
|
1112
1120
|
};
|
|
1113
1121
|
}
|
|
1122
|
+
function ldlFFT(logn, g00t, g01t, g11t) {
|
|
1123
|
+
// Algorithm 8: LDL*(G)
|
|
1124
|
+
// (Page 37)
|
|
1125
|
+
// Require: A full-rank self-adjoint matrix G = (Gᵢⱼ) ∈ FFT(Q[x]/(φ))²ˣ²
|
|
1126
|
+
// Ensure: The LDL* decomposition G = LDL* over FFT(Q[x]/(φ))
|
|
1127
|
+
// Format: All polynomials are in FFT representation.
|
|
1128
|
+
// 1: D₀₀ ← G₀₀
|
|
1129
|
+
// 2: L₁₀ ← G₁₀/G₀₀
|
|
1130
|
+
// 3: D₁₁ ← G₁₁ - L₁₀ ⊙ L₁₀* ⊙ G₀₀
|
|
1131
|
+
// 4: L ← [ 1 0 ; L₁₀ 1 ], D ← [ D₀₀ 0 ; 0 D₁₁ ]
|
|
1132
|
+
// 5: return (L, D)
|
|
1133
|
+
// Algorithm 9: ffLDL*(G)
|
|
1134
|
+
// (Page 37)
|
|
1135
|
+
// Require: A full-rank Gram matrix G ∈ FFT(Q[x]/(xⁿ + 1))²ˣ²
|
|
1136
|
+
// Ensure: A binary tree T
|
|
1137
|
+
// Format: All polynomials are in FFT representation.
|
|
1138
|
+
// 1: (L, D) ← LDL*(G) ▷ L = [ 1 0 ; L₁₀ 1 ], D = [ D₀₀ 0 ; 0 D₁₁ ]
|
|
1139
|
+
// 2: T.value ← L₁₀
|
|
1140
|
+
// 3: if (n = 2) then
|
|
1141
|
+
// 4: T.leftchild ← D₀₀
|
|
1142
|
+
// 5: T.rightchild ← D₁₁
|
|
1143
|
+
// 6: return T
|
|
1144
|
+
// 7: else
|
|
1145
|
+
// 8: d₀₀, d₀₁ ← splitfft(D₀₀) ▷ dᵢⱼ ∈ FFT(Q[x]/(x^{n/2} + 1))
|
|
1146
|
+
// 9: d₁₀, d₁₁ ← splitfft(D₁₁)
|
|
1147
|
+
// 10: G₀ ← [ d₀₀ d₀₁ ; d₀₁* d₀₀ ], G₁ ← [ d₁₀ d₁₁ ; d₁₁* d₁₀ ]
|
|
1148
|
+
// ▷ Since D₀₀, D₁₁ are self-adjoint, (3.30) applies
|
|
1149
|
+
// 11: T.leftchild ← ffLDL*(G₀) ▷ Recursive calls
|
|
1150
|
+
// 12: T.rightchild ← ffLDL*(G₁)
|
|
1151
|
+
// 13: return T
|
|
1152
|
+
// Recursive calls may alias g00t and g11t, and the top-level arrays persist across signing
|
|
1153
|
+
// retries. LDL replaces array entries, so shallow copies keep both kinds of caller state intact.
|
|
1154
|
+
g00t = g00t.slice();
|
|
1155
|
+
g01t = g01t.slice();
|
|
1156
|
+
g11t = g11t.slice();
|
|
1157
|
+
const hn = 1 << (logn - 1);
|
|
1158
|
+
for (let i = 0; i < hn; i++) {
|
|
1159
|
+
const g01 = g01t[i];
|
|
1160
|
+
const g11 = g11t[i];
|
|
1161
|
+
const mu = fComplex.scale(g01, 1.0 / g00t[i].re);
|
|
1162
|
+
g11t[i] = { re: g11.re - (mu.re * g01.re + mu.im * g01.im), im: g11.im };
|
|
1163
|
+
g01t[i] = fComplex.conj(mu);
|
|
1164
|
+
}
|
|
1165
|
+
return { g00: g00t, g01: g01t, g11: g11t };
|
|
1166
|
+
}
|
|
1114
1167
|
function ApproxExp(x, ccs) {
|
|
1115
1168
|
// Algorithm 13: ApproxExp(x, ccs), (Page 42)
|
|
1116
1169
|
// Require: Floating-point values x ∈ [0, ln(2)] and ccs ∈ [0, 1]
|
|
@@ -1633,19 +1686,24 @@ function genFalcon(opts) {
|
|
|
1633
1686
|
};
|
|
1634
1687
|
// [ 1B header ] [ 40B nonce ] [ compressed_sig ]
|
|
1635
1688
|
const SignatureCoderDetached = (logn) => {
|
|
1636
|
-
const
|
|
1637
|
-
const getSigLen = (s2) => (opts.padded ?
|
|
1689
|
+
const paddedSigLen = opts.sigLen - 1 - NONCELEN;
|
|
1690
|
+
const getSigLen = (s2) => (opts.padded ? paddedSigLen : s2.length);
|
|
1638
1691
|
return {
|
|
1639
1692
|
encode({ nonce, s2 }) {
|
|
1640
|
-
return headerCoder(0x30 + logn, splitCoder('falcon.detachedSignature', NONCELEN, getSigLen(s2))).encode([nonce, opts.padded ? pad(
|
|
1693
|
+
return headerCoder(0x30 + logn, splitCoder('falcon.detachedSignature', NONCELEN, getSigLen(s2))).encode([nonce, opts.padded ? pad(paddedSigLen).encode(s2) : s2]);
|
|
1641
1694
|
},
|
|
1642
1695
|
decode(data) {
|
|
1696
|
+
// Unpadded Round-3 signatures have parameter-set maxima (header + nonce + s2):
|
|
1697
|
+
// 752 bytes for Falcon-512 and 1462 for Falcon-1024. Reject before creating views or
|
|
1698
|
+
// entering the bit decoder so attacker-sized inputs cannot cause proportional work.
|
|
1699
|
+
if (!opts.padded && data.length > 1 + NONCELEN + opts.maxS2Len)
|
|
1700
|
+
throw new Error('detached signature too long');
|
|
1643
1701
|
// Padded detached signatures are fixed-length (`lengths.signature`), so the payload width
|
|
1644
1702
|
// must come from the parameter set, not from the input: deriving it would accept appended
|
|
1645
1703
|
// zero bytes and truncated padding as extra valid encodings of the same signature.
|
|
1646
1704
|
// Unpadded signatures are variable-length; decodeUnpaddedSig() enforces the exact canonical
|
|
1647
1705
|
// bitlength of whatever remains.
|
|
1648
|
-
const payloadLen = opts.padded ?
|
|
1706
|
+
const payloadLen = opts.padded ? paddedSigLen : data.length - NONCELEN - 1;
|
|
1649
1707
|
const [nonce, raw] = headerCoder(0x30 + logn, splitCoder('falcon.detachedSignature', NONCELEN, payloadLen)).decode(data);
|
|
1650
1708
|
const s2 = decodeSig(raw);
|
|
1651
1709
|
return { nonce, s2 };
|
|
@@ -1899,47 +1957,6 @@ function genFalcon(opts) {
|
|
|
1899
1957
|
return s + z;
|
|
1900
1958
|
}
|
|
1901
1959
|
}
|
|
1902
|
-
ldlFFT(logn, g00t, g01t, g11t) {
|
|
1903
|
-
// Algorithm 8: LDL*(G)
|
|
1904
|
-
// (Page 37)
|
|
1905
|
-
// Require: A full-rank self-adjoint matrix G = (Gᵢⱼ) ∈ FFT(Q[x]/(φ))²ˣ²
|
|
1906
|
-
// Ensure: The LDL* decomposition G = LDL* over FFT(Q[x]/(φ))
|
|
1907
|
-
// Format: All polynomials are in FFT representation.
|
|
1908
|
-
// 1: D₀₀ ← G₀₀
|
|
1909
|
-
// 2: L₁₀ ← G₁₀/G₀₀
|
|
1910
|
-
// 3: D₁₁ ← G₁₁ - L₁₀ ⊙ L₁₀* ⊙ G₀₀
|
|
1911
|
-
// 4: L ← [ 1 0 ; L₁₀ 1 ], D ← [ D₀₀ 0 ; 0 D₁₁ ]
|
|
1912
|
-
// 5: return (L, D)
|
|
1913
|
-
// Algorithm 9: ffLDL*(G)
|
|
1914
|
-
// (Page 37)
|
|
1915
|
-
// Require: A full-rank Gram matrix G ∈ FFT(Q[x]/(xⁿ + 1))²ˣ²
|
|
1916
|
-
// Ensure: A binary tree T
|
|
1917
|
-
// Format: All polynomials are in FFT representation.
|
|
1918
|
-
// 1: (L, D) ← LDL*(G) ▷ L = [ 1 0 ; L₁₀ 1 ], D = [ D₀₀ 0 ; 0 D₁₁ ]
|
|
1919
|
-
// 2: T.value ← L₁₀
|
|
1920
|
-
// 3: if (n = 2) then
|
|
1921
|
-
// 4: T.leftchild ← D₀₀
|
|
1922
|
-
// 5: T.rightchild ← D₁₁
|
|
1923
|
-
// 6: return T
|
|
1924
|
-
// 7: else
|
|
1925
|
-
// 8: d₀₀, d₀₁ ← splitfft(D₀₀) ▷ dᵢⱼ ∈ FFT(Q[x]/(x^{n/2} + 1))
|
|
1926
|
-
// 9: d₁₀, d₁₁ ← splitfft(D₁₁)
|
|
1927
|
-
// 10: G₀ ← [ d₀₀ d₀₁ ; d₀₁* d₀₀ ], G₁ ← [ d₁₀ d₁₁ ; d₁₁* d₁₀ ]
|
|
1928
|
-
// ▷ Since D₀₀, D₁₁ are self-adjoint, (3.30) applies
|
|
1929
|
-
// 11: T.leftchild ← ffLDL*(G₀) ▷ Recursive calls
|
|
1930
|
-
// 12: T.rightchild ← ffLDL*(G₁)
|
|
1931
|
-
// 13: return T
|
|
1932
|
-
g00t = g00t.slice(); // can be same as g11t and everything will break!
|
|
1933
|
-
const hn = 1 << (logn - 1);
|
|
1934
|
-
for (let i = 0; i < hn; i++) {
|
|
1935
|
-
const g01 = g01t[i];
|
|
1936
|
-
const g11 = g11t[i];
|
|
1937
|
-
const mu = fComplex.scale(g01, 1.0 / g00t[i].re);
|
|
1938
|
-
g11t[i] = { re: g11.re - (mu.re * g01.re + mu.im * g01.im), im: g11.im };
|
|
1939
|
-
g01t[i] = fComplex.conj(mu);
|
|
1940
|
-
}
|
|
1941
|
-
return { g00: g00t, g01: g01t, g11: g11t };
|
|
1942
|
-
}
|
|
1943
1960
|
splitFFT(logn, f) {
|
|
1944
1961
|
// Algorithm 1: splitfft(FFT(f))
|
|
1945
1962
|
// (Page 29)
|
|
@@ -2045,7 +2062,13 @@ function genFalcon(opts) {
|
|
|
2045
2062
|
// 13: z₀ ← mergefft(z'₀)
|
|
2046
2063
|
// 14: return z = (z₀, z₁)
|
|
2047
2064
|
if (logn === 0) {
|
|
2065
|
+
// The dynamic sampler stores 1/σ' instead of σ'. Keygen guarantees this interval,
|
|
2066
|
+
// but an imported compact key may reconstruct an invalid basis. Check the actual LDL*
|
|
2067
|
+
// leaf before it can drive SamplerZ; the negated comparison also rejects NaN/infinity.
|
|
2048
2068
|
const leaf = Math.sqrt(g00i[0].re) * INV_SIGMA[this.logn];
|
|
2069
|
+
const sigmaPrime = 1 / leaf;
|
|
2070
|
+
if (!(sigmaPrime >= SIGMA_MIN[this.logn] && sigmaPrime <= SIGMA_MAX))
|
|
2071
|
+
throw new Error('invalid secretKey: sampler sigma out of range');
|
|
2049
2072
|
// 3: z₀ ← SamplerZ(t₀, σ')
|
|
2050
2073
|
// ▷ Since n=1, tᵢ = invFFT(tᵢ) ∈ Q and zᵢ = invFFT(zᵢ) ∈ Z
|
|
2051
2074
|
const t0re = this.samplerZ(t0[0].re, leaf);
|
|
@@ -2053,7 +2076,7 @@ function genFalcon(opts) {
|
|
|
2053
2076
|
return { t0: [{ re: t0re, im: 0.0 }], t1: [{ re: t1re, im: 0.0 }] };
|
|
2054
2077
|
}
|
|
2055
2078
|
// 6: (l, T₀, T₁) ← (T.value, T.leftchild, T.rightchild)
|
|
2056
|
-
const { g00, g01, g11 } =
|
|
2079
|
+
const { g00, g01, g11 } = ldlFFT(logn, g00i, g01i, g11i);
|
|
2057
2080
|
const { f0: g00f0, f1: g00f1 } = this.splitSelfAdjFFT(logn, g00);
|
|
2058
2081
|
const { f0: g11f0, f1: g11f1 } = this.splitSelfAdjFFT(logn, g11);
|
|
2059
2082
|
// 7: t'₁ ← splitfft(t₁)
|
|
@@ -2203,10 +2226,15 @@ function genFalcon(opts) {
|
|
|
2203
2226
|
publicKey: publicKeyCoder.bytesLen,
|
|
2204
2227
|
secretKey: secretKeyCoder.bytesLen,
|
|
2205
2228
|
});
|
|
2229
|
+
// Falcon takes a sampler callback the other schemes do not, and rejects `context`
|
|
2230
|
+
// with its own message; both stay in the accepted set so the specific errors fire.
|
|
2231
|
+
const FALCON_SIG_OPT_KEYS = [...SIG_OPT_KEYS, 'random'];
|
|
2206
2232
|
// Noble exposes a 48-byte sampler-seed hook,
|
|
2207
2233
|
// but Falcon still samples/encodes a separate 40-byte nonce per signature.
|
|
2208
2234
|
const getRnd = (opts = {}) => {
|
|
2209
|
-
|
|
2235
|
+
// `context` stays in the accepted set so the specific "not supported" error below
|
|
2236
|
+
// still fires, rather than the generic unexpected-option one.
|
|
2237
|
+
opts = validateSigOpts(opts, FALCON_SIG_OPT_KEYS);
|
|
2210
2238
|
if (opts.context !== undefined)
|
|
2211
2239
|
throw new Error('context is not supported');
|
|
2212
2240
|
if (opts.random !== undefined && typeof opts.random !== 'function')
|
|
@@ -2221,8 +2249,8 @@ function genFalcon(opts) {
|
|
|
2221
2249
|
return (len = 0) => drbg.randomBytes(len);
|
|
2222
2250
|
};
|
|
2223
2251
|
const checkVerOpts = (opts = {}) => {
|
|
2224
|
-
validateVerOpts(opts);
|
|
2225
|
-
if (
|
|
2252
|
+
const normalized = validateVerOpts(opts);
|
|
2253
|
+
if (normalized.context !== undefined)
|
|
2226
2254
|
throw new Error('context is not supported');
|
|
2227
2255
|
};
|
|
2228
2256
|
const tests = Object.freeze({
|
|
@@ -2296,12 +2324,43 @@ function genFalcon(opts) {
|
|
|
2296
2324
|
},
|
|
2297
2325
|
open(sig, pk, verOpts = {}) {
|
|
2298
2326
|
checkVerOpts(verOpts);
|
|
2299
|
-
|
|
2300
|
-
//
|
|
2301
|
-
//
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2327
|
+
// Wrong argument types are caller bugs and must stay TypeErrors; only what happens
|
|
2328
|
+
// after this is untrusted input. Detached verify() type-checks the public key the same
|
|
2329
|
+
// way, so open() does too: a wrong type is fatal, and a malformed (wrong-length or
|
|
2330
|
+
// non-canonical) key folds into the single rejection below rather than leaking a raw
|
|
2331
|
+
// codec error, exactly as detached verify() folds it into `false`.
|
|
2332
|
+
abytes(sig, undefined, 'signature');
|
|
2333
|
+
abytes(pk, undefined, 'publicKey');
|
|
2334
|
+
// Decode and verify owned snapshots. Apart from keeping the authenticated result stable
|
|
2335
|
+
// after open() returns, this ensures every verification step observes the same bytes when
|
|
2336
|
+
// an input is backed by SharedArrayBuffer or has subclass-overridden view methods.
|
|
2337
|
+
const ownedSig = copyBytes(sig);
|
|
2338
|
+
const ownedPk = copyBytes(pk);
|
|
2339
|
+
// Decode failures and malformed-key failures are rejected signatures, not internal
|
|
2340
|
+
// faults. Letting the codec's own errors out gave a caller handling untrusted input
|
|
2341
|
+
// several different messages for one corrupt byte, including "end of buffer: len=2
|
|
2342
|
+
// buf=0 lastByte=undefined", which reads as a library bug. Detached verify already
|
|
2343
|
+
// treats every such failure uniformly; open() collapses them into one Error (the
|
|
2344
|
+
// original preserved as `cause`). A well-formed signature that simply does not
|
|
2345
|
+
// validate falls through to the same message with no cause.
|
|
2346
|
+
try {
|
|
2347
|
+
let verifiedMsg;
|
|
2348
|
+
try {
|
|
2349
|
+
const { s2, nonce, msg } = SignatureCoder.decode(ownedSig);
|
|
2350
|
+
if (verifyRaw(ownedPk, s2, nonce, msg))
|
|
2351
|
+
verifiedMsg = msg;
|
|
2352
|
+
}
|
|
2353
|
+
catch (cause) {
|
|
2354
|
+
throw new Error('invalid signature', { cause });
|
|
2355
|
+
}
|
|
2356
|
+
if (verifiedMsg === undefined)
|
|
2357
|
+
throw new Error('invalid signature');
|
|
2358
|
+
// Do not retain or expose the full attached-signature allocation through `.buffer`.
|
|
2359
|
+
return copyBytes(verifiedMsg);
|
|
2360
|
+
}
|
|
2361
|
+
finally {
|
|
2362
|
+
cleanBytes(ownedSig, ownedPk);
|
|
2363
|
+
}
|
|
2305
2364
|
},
|
|
2306
2365
|
});
|
|
2307
2366
|
const res = {
|
|
@@ -2318,14 +2377,14 @@ function genFalcon(opts) {
|
|
|
2318
2377
|
}
|
|
2319
2378
|
const falcon512opts = {
|
|
2320
2379
|
N: 512,
|
|
2380
|
+
// Keep the mode an own property: omitted config fields must not inherit from Object.prototype.
|
|
2381
|
+
padded: false,
|
|
2321
2382
|
// Table 3.3 fixed padded detached bytes, including the detached header byte and 40-byte nonce.
|
|
2322
2383
|
sigLen: 666,
|
|
2323
2384
|
fgBits: 6,
|
|
2324
2385
|
FGBits: 8,
|
|
2325
2386
|
// Compressed-s payload bytes only, excluding the detached header byte and 40-byte nonce.
|
|
2326
2387
|
paddedLen: 625,
|
|
2327
|
-
// Payload-only budget: genFalcon() adds the detached header byte and 40-byte nonce around it.
|
|
2328
|
-
detachedLen: 690,
|
|
2329
2388
|
};
|
|
2330
2389
|
/**
|
|
2331
2390
|
* Falcon-512 detached-signature API with the attached helper exposed as `.attached`.
|
|
@@ -2357,14 +2416,13 @@ export const falcon512padded = /* @__PURE__ */ (() => genFalcon({
|
|
|
2357
2416
|
}))();
|
|
2358
2417
|
const falcon1024opts = {
|
|
2359
2418
|
N: 1024,
|
|
2419
|
+
padded: false,
|
|
2360
2420
|
// Table 3.3 fixed padded detached bytes, including the detached header byte and 40-byte nonce.
|
|
2361
2421
|
sigLen: 1280,
|
|
2362
2422
|
fgBits: 5,
|
|
2363
2423
|
FGBits: 8,
|
|
2364
2424
|
// Compressed-s payload bytes only, excluding the detached header byte and 40-byte nonce.
|
|
2365
2425
|
paddedLen: 1239,
|
|
2366
|
-
// Payload-only budget: genFalcon() adds the detached header byte and 40-byte nonce around it.
|
|
2367
|
-
detachedLen: 1280,
|
|
2368
2426
|
};
|
|
2369
2427
|
/**
|
|
2370
2428
|
* Falcon-1024 detached-signature API with the attached helper exposed as `.attached`.
|
|
@@ -2405,6 +2463,7 @@ export const __tests = /* @__PURE__ */ (() => Object.freeze({
|
|
|
2405
2463
|
INV_SIGMA,
|
|
2406
2464
|
SIGMA_MIN,
|
|
2407
2465
|
getFloatPoly,
|
|
2466
|
+
ldlFFT,
|
|
2408
2467
|
cleanCPoly,
|
|
2409
2468
|
falcon512: falcon512.__test,
|
|
2410
2469
|
falcon512padded: falcon512padded.__test,
|
package/hybrid.d.ts
CHANGED
|
@@ -84,6 +84,12 @@ type CurveSign = ECDSA | EdDSA;
|
|
|
84
84
|
/**
|
|
85
85
|
* Wraps an ECDH-capable curve as a KEM.
|
|
86
86
|
* Shared secrets stay in the wrapped curve's raw ECDH byte format with no built-in KDF.
|
|
87
|
+
*
|
|
88
|
+
* SECURITY: this is a low-level component adapter, not a standalone IND-CCA-secure KEM. It does
|
|
89
|
+
* not bind the encapsulation or recipient public key into the secret, so distinct accepted point
|
|
90
|
+
* encodings can produce the same output. Use it only inside a construction whose specified
|
|
91
|
+
* combiner binds those values, or use a standardized DHKEM with labeled extract-and-expand.
|
|
92
|
+
*
|
|
87
93
|
* On SEC 1 / Weierstrass curves, that means the compressed shared-point body without the
|
|
88
94
|
* 1-byte `0x02` / `0x03` prefix.
|
|
89
95
|
* The X25519 path also leaves RFC 7748's optional all-zero shared-secret check to callers.
|
|
@@ -99,12 +105,12 @@ type CurveSign = ECDSA | EdDSA;
|
|
|
99
105
|
* Wrap an ECDH-capable curve as a generic KEM.
|
|
100
106
|
* ```ts
|
|
101
107
|
* import { x25519 } from '@noble/curves/ed25519.js';
|
|
102
|
-
* import {
|
|
103
|
-
* const kem =
|
|
108
|
+
* import { _ecdhKem } from '@noble/post-quantum/hybrid.js';
|
|
109
|
+
* const kem = _ecdhKem(x25519);
|
|
104
110
|
* const publicKeyLen = kem.lengths.publicKey;
|
|
105
111
|
* ```
|
|
106
112
|
*/
|
|
107
|
-
export declare function
|
|
113
|
+
export declare function _ecdhKem(curve: CurveECDH, allowZeroKey?: boolean): TRet<KEM>;
|
|
108
114
|
/**
|
|
109
115
|
* Wraps a curve signer as a generic `Signer`.
|
|
110
116
|
* Signatures stay in the wrapped curve's native byte encoding.
|
|
@@ -151,12 +157,18 @@ export declare function expandSeedXof(xof: TArg<XOF>): TRet<ExpandSeed>;
|
|
|
151
157
|
export type Combiner = (publicKeys: TArg<Uint8Array[]>, cipherTexts: TArg<Uint8Array[]>, sharedSecrets: TArg<Uint8Array[]>) => TRet<Uint8Array>;
|
|
152
158
|
/**
|
|
153
159
|
* Combines multiple KEMs into one composite KEM.
|
|
154
|
-
* @param realSeedLen -
|
|
155
|
-
*
|
|
160
|
+
* @param realSeedLen - Positive input seed length expected by `expandSeed`, or `undefined` to use
|
|
161
|
+
* the sum of component seed lengths. Callers remain responsible for choosing a security-appropriate
|
|
162
|
+
* size.
|
|
163
|
+
* @param realMsgLen - Positive shared-secret length returned by `combiner`, or `undefined` to use
|
|
164
|
+
* the sum of component message lengths.
|
|
156
165
|
* @param expandSeed - Seed expander used to derive per-KEM seeds.
|
|
157
166
|
* @param combiner - Combines the per-KEM outputs into one shared secret.
|
|
158
|
-
* @param kems - KEM
|
|
167
|
+
* @param kems - At least one KEM implementation. A construction advertised as hybrid normally
|
|
168
|
+
* supplies two or more.
|
|
159
169
|
* @returns Composite KEM.
|
|
170
|
+
* @throws On wrong argument types. {@link TypeError}
|
|
171
|
+
* @throws If there are no components or any required length resolves to zero. {@link RangeError}
|
|
160
172
|
* @example
|
|
161
173
|
* Combine multiple KEMs into one composite KEM.
|
|
162
174
|
* ```ts
|
|
@@ -179,10 +191,15 @@ realMsgLen: number | undefined, // how much bytes combiner returns
|
|
|
179
191
|
expandSeed: TArg<ExpandSeed>, combiner: TArg<Combiner>, ...kems: TArg<KEM[]>): TRet<KEM>;
|
|
180
192
|
/**
|
|
181
193
|
* Combines multiple signers into one composite signer.
|
|
182
|
-
* @param realSeedLen -
|
|
194
|
+
* @param realSeedLen - Positive input seed length expected by `expandSeed`, or `undefined` to use
|
|
195
|
+
* the sum of component seed lengths. Callers remain responsible for choosing a security-appropriate
|
|
196
|
+
* size.
|
|
183
197
|
* @param expandSeed - Seed expander used to derive per-signer seeds.
|
|
184
|
-
* @param signers -
|
|
198
|
+
* @param signers - At least one signer. A construction advertised as hybrid normally supplies two
|
|
199
|
+
* or more.
|
|
185
200
|
* @returns Composite signer.
|
|
201
|
+
* @throws On wrong argument types. {@link TypeError}
|
|
202
|
+
* @throws If there are no components or any required length resolves to zero. {@link RangeError}
|
|
186
203
|
* @example
|
|
187
204
|
* Combine multiple signers into one composite signer.
|
|
188
205
|
* ```ts
|
|
@@ -211,14 +228,16 @@ export declare function combineSigners(realSeedLen: number | undefined, expandSe
|
|
|
211
228
|
* @param xof - XOF used for seed expansion.
|
|
212
229
|
* @param kdf - Hash used for the final combiner.
|
|
213
230
|
* @returns Hybrid KEM.
|
|
231
|
+
* @throws On wrong argument types. {@link TypeError}
|
|
232
|
+
* @throws On wrong argument ranges or values. {@link RangeError}
|
|
214
233
|
* @example
|
|
215
234
|
* Build a QSF hybrid KEM preset from a PQ KEM and an elliptic-curve KEM.
|
|
216
235
|
* ```ts
|
|
217
236
|
* import { p256 } from '@noble/curves/nist.js';
|
|
218
237
|
* import { sha3_256, shake256 } from '@noble/hashes/sha3.js';
|
|
219
|
-
* import { QSF,
|
|
238
|
+
* import { QSF, _ecdhKem } from '@noble/post-quantum/hybrid.js';
|
|
220
239
|
* import { ml_kem768 } from '@noble/post-quantum/ml-kem.js';
|
|
221
|
-
* const kem = QSF('example', ml_kem768,
|
|
240
|
+
* const kem = QSF('example', ml_kem768, _ecdhKem(p256, true), shake256, sha3_256);
|
|
222
241
|
* const publicKeyLen = kem.lengths.publicKey;
|
|
223
242
|
* ```
|
|
224
243
|
*/
|
|
@@ -241,15 +260,17 @@ export declare const QSF_ml_kem1024_p384: TRet<KEM>;
|
|
|
241
260
|
* @param xof - XOF used for seed expansion.
|
|
242
261
|
* @param hash - Hash used for HKDF extraction and expansion.
|
|
243
262
|
* @returns Hybrid KEM.
|
|
263
|
+
* @throws On wrong argument types. {@link TypeError}
|
|
264
|
+
* @throws On wrong argument ranges or values. {@link RangeError}
|
|
244
265
|
* @example
|
|
245
266
|
* Build the "KitchenSink" hybrid KEM combiner.
|
|
246
267
|
* ```ts
|
|
247
268
|
* import { sha256 } from '@noble/hashes/sha2.js';
|
|
248
269
|
* import { shake256 } from '@noble/hashes/sha3.js';
|
|
249
|
-
* import { createKitchenSink,
|
|
270
|
+
* import { createKitchenSink, _ecdhKem } from '@noble/post-quantum/hybrid.js';
|
|
250
271
|
* import { ml_kem768 } from '@noble/post-quantum/ml-kem.js';
|
|
251
272
|
* import { x25519 } from '@noble/curves/ed25519.js';
|
|
252
|
-
* const kem = createKitchenSink('example', ml_kem768,
|
|
273
|
+
* const kem = createKitchenSink('example', ml_kem768, _ecdhKem(x25519), shake256, sha256);
|
|
253
274
|
* const publicKeyLen = kem.lengths.publicKey;
|
|
254
275
|
* ```
|
|
255
276
|
*/
|