sm-crypto: Predictable SM2 key generation in Node.js: default RNG uses Math.random + wall clock
Collected exploitation and prediction evidence
Risk source coverageCISA KEV is observed exploitation evidence. FIRST EPSS is a dated prediction of exploitation in the next 30 days. Missing scores are unknown. This collector requests predictions only for explicit CVE aliases in the public NuGet archive; Microsoft release views retain their existing prediction scope.
No collected risk observation for these identifiers. This does not establish absence of exploitation or a zero probability.
cve-program · CVE-2026-73567
Publishedsm-crypto provides JavaScript implementations of the Chinese cryptographic algorithms SM2, SM3, and SM4. Prior to 0.5.0, the default no-argument sm2.generateKeyPairHex() path in Node.js uses the module-wide SecureRandom instance in src/sm2/utils.js, supplied by [email protected], which seeds an ARC4 stream from Math.random() and new Date().getTime() because window.crypto.getRandomValues is unavailable even though globalThis.crypto exists. An attacker who can observe the process's Math.random() outputs and estimate the key-generation time can reconstruct the seed, recover generated SM2 private keys, and predict signing ephemeral scalars used to forge signatures. This issue is fixed in version 0.5.0.
- Source published
- 13 Aug 2026 17:40:54 UTC
- Source modified
- 14 Aug 2026 18:09:20 UTC
- Observed here
- 23 Oct 2026 15:48:27 UTC
- Source terms
- CVE Program Terms of Use
Original source record · Cite this exact observation · Complete original publisher JSON
This view projects the publisher's source fields for reading; the original JSON retains additional fields and CVE enrichment containers. Attribution: cve-program · CVE Program Terms of Use. Source text is retained; field grouping and change labels are provided by ChangeIntel.
CVSS 3.1 · 9.1 CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N
nvd · CVE-2026-73567
Publishedsm-crypto provides JavaScript implementations of the Chinese cryptographic algorithms SM2, SM3, and SM4. Prior to 0.5.0, the default no-argument sm2.generateKeyPairHex() path in Node.js uses the module-wide SecureRandom instance in src/sm2/utils.js, supplied by [email protected], which seeds an ARC4 stream from Math.random() and new Date().getTime() because window.crypto.getRandomValues is unavailable even though globalThis.crypto exists. An attacker who can observe the process's Math.random() outputs and estimate the key-generation time can reconstruct the seed, recover generated SM2 private keys, and predict signing ephemeral scalars used to forge signatures. This issue is fixed in version 0.5.0.
NVD analysis and CPE criteria are source context, not proof that a package or your environment is affected. Complete affected data, logical configurations and SSVC are retained in the original JSON.
Source analysis status: Deferred
Weakness classifications: CWE-338
Original publisher date fields
published: 2026-08-13T18:18:19.387
lastModified: 2026-10-24T15:10:00.190
- Source published
- 13 Aug 2026 18:18:19 UTC
- Source modified
- 24 Oct 2026 15:10:00 UTC
- Observed here
- 25 Oct 2026 15:19:09 UTC
- Source terms
- NIST NVD usage terms; underlying source attribution retained
Original source record · Cite this exact observation · Complete original publisher JSON
This view projects the publisher's source fields for reading; the original JSON retains additional fields and CVE enrichment containers. Attribution: nvd · NIST NVD usage terms; underlying source attribution retained. Source text is retained; field grouping and change labels are provided by ChangeIntel.
CVSS 3.1 · [email protected] · Secondary · 9.1 CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N
osv · GHSA-vh45-f885-3848
Published## Summary `sm-crypto` (npm package **0.4.0**, the latest release, published 2026-01-20) generates SM2 private keys and signing ephemeral scalars from a single module-wide RNG instance (`src/sm2/utils.js`: `const rng = new SecureRandom()`). `SecureRandom` is jsbn's PRNG, which seeds an **ARC4** stream from `window.crypto.getRandomValues` when available. **In Node.js — sm-crypto's primary runtime — `window` is `undefined`, so the CSPRNG branch is skipped** and the seed pool is instead filled from `Math.random()` (V8 `xorshift128+`, recoverable from a few outputs) plus `new Date().getTime()` (wall clock, attacker-estimable). Node *does* expose Web Crypto as `globalThis.crypto`, but jsbn checks `window.crypto`, not `globalThis.crypto`, so the secure path is never taken. Consequently every SM2 private key produced by the default `sm2.generateKeyPairHex()` and every signing ephemeral scalar is derived from non-cryptographic sources and is **predictable** by an attacker who can observe a few `Math.random()` outputs and estimate the generation time. This is the library's **default** (no-argument) path; no caller-selected parameter or configuration is required to trigger it. It is reproduced end-to-end against the unmodified real npm packages (`[email protected]` + `[email protected]`); the PoC below runs against the real installed package, not a copy. The defect is still present on the latest published version (0.4.0) and is not covered by any existing `JuneAndGreen/sm-crypto` issue (0 afldl issues exist; the most recent issues are unrelated SM3/HKDF/PBKDF2 feature requests). ## Details `[email protected]` `index.js` — RNG pool initialization (fallback taken in Node): ```js if (rng_pool == null) { rng_pool = new Array(); rng_pptr = 0; var t; if (typeof window !== "undefined" && window.crypto) { // <-- false in Node if (window.crypto.getRandomValues) { /* webcrypto */ } ... } while (rng_pptr < rng_psize) { // <-- fallback path t = Math.floor(65536 * Math.random()); // Math.random() rng_pool[rng_pptr++] = t >>> 8; rng_pool[rng_pptr++] = t & 255; } rng_pptr = 0; rng_seed_time(); // + Date.getTime() } ``` `sm-crypto` `src/sm2/utils.js`: ```js const { SecureRandom } = require('jsbn'); const rng = new SecureRandom(); // single module-wide RNG ... function generateKeyPairHex(a, b, c) { const random = a ? new BigInteger(a, b, c) : new BigInteger(n.bitLength(), rng); // uses rng const d = random.mod(n.subtract(BigInteger.ONE)).add(BigInteger.ONE); // private key ... } ``` The default (no-argument) call path uses `rng`, the jsbn ARC4 instance seeded from `Math.random()` + time. The same `rng` feeds the signing ephemeral scalar during SM2 signing. ## PoC The PoC runs against the real installed npm packages. It pins `Math.random` and `Date` **before** `require('sm-crypto')` so jsbn's seed pool is built from controlled inputs. Three independent fresh Node processes then produce the **same** SM2 private key, proving the key is a pure deterministic function of those non-cryptographic sources. It also prints a probe confirming the fallback branch is taken in Node. ### One-line reproducer ```bash WORK=$(mktemp -d) && cd "$WORK" && npm init -y >/dev/null \ && npm install [email protected] [email protected] >/dev/null \ && export NODE_PATH="$WORK/node_modules" \ && node poc.js probe && node poc.js deterministic && node poc.js deterministic ``` ### `poc.js` ```js /* * PoC for sm-crypto predictable default RNG in Node.js. * * sm-crypto (npm 0.4.0) generates SM2 private keys / ephemeral scalars using * jsbn's SecureRandom. In a browser jsbn seeds ARC4 from window.crypto, but in * Node.js `window` is undefined so the CSPRNG branch is skipped and the pool is * filled from Math.random() plus new Date().getTime(). Both are * non-cryptographic; the time is attacker-estimable and V8's Math.random is a * recoverable xorshift128+ stream. Consequently SM2 keys produced by the * default path are predictable. * * This PoC proves the key is a deterministic function of those two inputs: we * pin Math.random and the clock to fixed values BEFORE sm-crypto (and therefore * jsbn) is loaded, then generate a keypair. Re-running with the same pinned * values reproduces the exact same private key. */ const MODE = process.argv[2] || 'probe'; // 'probe' | 'deterministic' if (MODE === 'deterministic') { // --- pin entropy sources BEFORE requiring sm-crypto/jsbn --- const fixedTime = 1700000000000; let s = 0x12345678 >>> 0; Math.random = function () { // tiny deterministic LCG standing in for the (already non-crypto) Math.random s = (Math.imul(s, 1103515245) + 12345) >>> 0; return s / 0x100000000; }; const RealDate = globalThis.Date; class FixedDate extends RealDate { constructor(...a) { super(...(a.length === 0 ? [fixedTime] : a)); } } FixedDate.now = () => fixedTime; globalThis.Date = FixedDate; } const sm2 = require('sm-crypto').sm2; const kp = sm2.generateKeyPairHex(); console.log('PRIVATE=' + kp.privateKey); if (MODE === 'probe') { console.log('--- probe ---'); console.log('typeof window =', typeof window, '(undefined in Node => jsbn CSPRNG branch skipped)'); console.log('typeof globalThis.crypto =', typeof globalThis.crypto, '(Node Web Crypto exists but jsbn checks window.crypto, not globalThis.crypto)'); console.log('Math.random sample =', Math.random()); console.log('Date.now() =', Date.now(), '(attacker-estimable, mixed into ARC4 seed)'); } ``` Real captured output: ``` ===== PROBE (real default path, no patching) ===== PRIVATE=6072e45733a4187791ec28ce906fef18c7d33c8529969e1a852833c4349cfc38 --- probe --- typeof window = undefined (undefined in Node => jsbn CSPRNG branch skipped) typeof globalThis.crypto = object (Node Web Crypto exists but jsbn checks window.crypto, not globalThis.crypto) Math.random sample = 0.704452488761137 Date.now() = 1784455686795 (attacker-estimable, mixed into ARC4 seed) ===== DETERMINISTIC (Math.random + Date pinned before require sm-crypto) ===== --- run #1 --- PRIVATE=143268fa0939b4da09eab8c9a2e027a04555b6c433fef4f54fc5edd517c0a6b1 --- run #2 --- PRIVATE=143268fa0939b4da09eab8c9a2e027a04555b6c433fef4f54fc5edd517c0a6b1 ``` The two deterministic runs produce the **identical** SM2 private key, demonstrating the key is a pure function of `Math.random()` + wall-clock time. ## Impact **Private-key recovery / signature forgery of any SM2 keypair generated with the default API in Node.js.** This is the most serious class of defect for a maintained SM2 library: the *default* key-generation path is non-cryptographic on its primary runtime. - **Private-key recovery.** Any SM2 keypair generated with the default API in Node is derived from `Math.random()` + wall-clock time. An attacker who can observe a few `Math.random()` outputs (V8 `xorshift128+` state is recoverable from ~4 observed doubles) and estimate the generation time can reproduce the private key and forge signatures. - **Signing ephemeral reuse / forgery.** The same RNG feeds the ephemeral scalar `k` during SM2 signing; a predictable `k` leaks the private key from a single signature (SM2 is EC-Schnorr-like: `s = (k^-1)(e + d·r) mod n`). - Pre-authentication / no privilege required: anyone who can induce a victim to generate a key or sign a message (the normal API use) is positioned to predict the secret material. ### Suggested fix Seed the RNG from a CSPRNG in Node. The simplest fix in `sm-crypto` is to replace the jsbn ARC4 instance with Web Crypto / `crypto.randomBytes`: ```js // src/sm2/utils.js const nodeCrypto = (typeof require === 'function') ? require('crypto') : null; function csrandBytes(n) { if (nodeCrypto) return nodeCrypto.randomBytes(n); // Node if (globalThis.crypto) { // Web Crypto (browser/Node ≥ 19) const b = new Uint8Array(n); globalThis.crypto.getRandomValues(b); return b; } throw new Error('no CSPRNG available'); } ``` and use it to generate the private key / ephemeral directly, or to reseed the jsbn pool. A separate (upstream) fix belongs in jsbn to check `globalThis.crypto` in addition to `window.crypto`. ### Affected versions - npm `sm-crypto` **0.4.0** (latest, published 2026-01-20). Depends on `jsbn ^1.1.0` (`[email protected]`, whose `index.js` RNG is the root cause). - Runtime: Node.js (the primary runtime; in a browser the jsbn CSPRNG branch is taken). ## Credit Reported by the diff/ambidiff security research effort (afldl).
- Source published
- 24 Jul 2026 21:50:45 UTC
- Source modified
- 13 Aug 2026 17:55:56 UTC
- Observed here
- 24 Oct 2026 13:17:17 UTC
- Source terms
- GitHub Advisory Database · CC-BY-4.0
Original source record · Cite this exact observation · Complete original publisher JSON
This view projects the publisher's source fields for reading; the original JSON retains additional fields and CVE enrichment containers. Attribution: osv · GitHub Advisory Database · CC-BY-4.0. Source text is retained; field grouping and change labels are provided by ChangeIntel.
CVSS_V3 · CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N
Publisher rating · CRITICAL