sm-crypto: Node.js에서 예측 가능한 SM2 키 생성: 기본 RNG는 Math.random + 벽시계를 사용합니다.
sm-crypto(npm 패키지 0.4.0, 최신 릴리스, 2026-01-20 게시)
SM2 개인 키를 생성하고 단일에서 임시 스칼라에 서명합니다.
모듈 전체 RNG 인스턴스(src/sm2/utils.js: const rng = new SecureRandom()).
SecureRandom은 jsbn의 PRNG로 ARC4 스트림을 시드합니다.
가능한 경우 window.crypto.getRandomValues. Node.js에서 — sm-crypto의
기본 런타임 — window는 정의되지 않음이므로 CSPRNG 분기를 건너뜁니다
대신 시드 풀은 Math.random()(V8 xorshift128+,
몇 가지 출력에서 복구 가능) 및 new Date().getTime()(벽시계,
공격자 추정 가능).
노드는 Web Crypto를 globalThis.crypto로 노출하지만 jsbn은 확인합니다.
globalThis.crypto가 아닌 window.crypto이므로 보안 경로가 사용되지 않습니다.
결과적으로 기본적으로 생성된 모든 SM2 개인 키
sm2.generateKeyPairHex() 및 모든 서명 임시 스칼라는 다음에서 파생됩니다.
비암호화 소스이며 공격자가 관찰할 수 있는 예측 가능
몇 가지 Math.random() 출력을 생성하고 생성 시간을 추정합니다.
이는 라이브러리의 기본(인수 없음) 경로입니다. 발신자가 선택되지 않았습니다.
이를 트리거하려면 매개변수 또는 구성이 필요합니다. 재현되어 있습니다
수정되지 않은 실제 npm 패키지(sm-crypto@0.4.0 +
jsbn@1.1.0); 아래 PoC는 실제 설치된 패키지에 대해 실행됩니다.
복사. 최신 게시 버전(0.4.0)에는 결함이 여전히 존재하며
기존 JuneAndGreen/sm-crypto 문제가 적용되지 않습니다(0 afldl 문제
존재하다; 가장 최근의 문제는 관련 없는 SM3/HKDF/PBKDF2 기능 요청입니다.
jsbn@1.1.0 index.js — RNG 풀 초기화(노드에서 대체):
``js if (rng_pool == null) { rng_pool = 새로운 배열(); rng_pptr = 0; var t; if (typeof window !== "undefine" && window.crypto) { // <-- 노드에서 false if (window.crypto.getRandomValues) { /* webcrypto */ } ... } while (rng_pptr < rng_psize) { // <-- 대체 경로 t = Math.floor(65536 * Math.random()); // 수학.랜덤() rng_pool[rng_pptr++] = t >>> 8; rng_pool[rng_pptr++] = t & 255; } rng_pptr = 0; rng_seed_time(); // + 날짜.getTime() }
`sm-crypto` `src/sm2/utils.js`:
``js
const { SecureRandom } = require('jsbn');
const rng = 새로운 SecureRandom(); // 단일 모듈 전체 RNG
...
함수 generateKeyPairHex(a, b, c) {
const 무작위 = ? 새로운 BigInteger(a, b, c)
: new BigInteger(n.bitLength(), rng); // rng를 사용한다
const d = random.mod(n.subtract(BigInteger.ONE)).add(BigInteger.ONE); // 개인 키
...
}
기본(인수 없음) 호출 경로는 시드된 jsbn ARC4 인스턴스인 'rng'를 사용합니다.
Math.random() + 시간에서. 동일한 'rng'가 서명 임시 항목에 피드를 제공합니다.
SM2 서명 중 스칼라.
PoC는 실제 설치된 npm 패키지에 대해 실행됩니다. 'Math.random'이 고정됩니다.
그리고 날짜 이전 require('sm-crypto')이므로 jsbn의 시드 풀은 다음에서 구축됩니다.
제어된 입력. 세 개의 독립적인 새로운 노드 프로세스가 생성됩니다.
동일 SM2 개인 키는 키가 순수한 결정론적 기능임을 증명합니다.
암호화되지 않은 소스. 또한 폴백을 확인하는 프로브도 인쇄합니다.
Branch는 Node에서 가져옵니다.
``배쉬
WORK=$(mktemp -d) && cd "$WORK" && npm init -y >/dev/null
&& npm install sm-crypto@0.4.0 jsbn@1.1.0 >/dev/null
&& 내보내기 NODE_PATH="$WORK/node_modules"
&& node poc.js 프로브 && node poc.js 결정적 && node poc.js 결정적
### `poc.js`
``js
/*
* Node.js의 sm-crypto 예측 가능한 기본 RNG에 대한 PoC.
*
* sm-crypto(npm 0.4.0)는 다음을 사용하여 SM2 개인 키/임시 스칼라를 생성합니다.
* jsbn의 SecureRandom. 브라우저 jsbn에서는 window.crypto의 ARC4를 시드하지만
* Node.js `window`는 정의되지 않았으므로 CSPRNG 분기를 건너뛰고 풀은
* Math.random()과 new Date().getTime()으로 채워집니다. 둘 다
* 비암호화; 시간은 공격자가 추정할 수 있으며 V8의 Math.random은
* 복구 가능한 xorshift128+ 스트림. 결과적으로 SM2 키는
* 기본 경로는 예측 가능합니다.
*
* 이 PoC는 키가 두 입력의 결정론적 기능임을 증명합니다.
* sm-crypto 이전에 Math.random 및 시계를 고정 값으로 고정합니다(따라서
* jsbn)이 로드된 다음 키 쌍을 생성합니다. 동일한 고정으로 다시 실행
* 값은 정확히 동일한 개인 키를 재현합니다.
*/
const MODE = process.argv[2] || '조사'; // '프로브' | '결정론적'
if (MODE === '결정적') {
// --- sm-crypto/jsbn을 요구하기 전에 엔트로피 소스를 고정합니다 ---
const 고정시간 = 1700000000000;
s = 0x12345678 >>> 0;
Math.random = 함수() {
// (이미 암호화폐가 아닌) Math.random을 대신하는 작은 결정론적 LCG
s = (Math.imul(s, 1103515245) + 12345) >>> 0;
s / 0x100000000을 반환합니다.
};
const RealDate = globalThis.Date;
클래스 FixDate는 RealDate를 확장합니다.
constructor(...a) { super(...(a.length === 0 ? [fixedTime] : a)); }
}
고정날짜.now = () => 고정시간;
globalThis.Date = 고정 날짜;
}
const sm2 = require('sm-crypto').sm2;
const kp = sm2.generateKeyPairHex();
console.log('PRIVATE=' + kp.privateKey);
if (MODE === '프로브') {
console.log('--- 조사 ---');
console.log('typeof window =', typeof window, '(노드에서 정의되지 않음 => jsbn CSPRNG 분기를 건너뛰었습니다.)');
console.log('typeof globalThis.crypto =', typeof globalThis.crypto, '(Node Web Crypto가 존재하지만 jsbn은 globalThis.crypto가 아니라 window.crypto를 확인합니다)');
console.log('Math.random 샘플 =', Math.random());
console.log('Date.now() =', Date.now(), '(공격자 추정 가능, ARC4 시드에 혼합)');
}
실제 캡처된 출력:
===== 조사(실제 기본 경로, 패치 없음) =====
개인=6072e45733a4187791ec28ce906fef18c7d33c8529969e1a852833c4349cfc38
--- 프로브 ---
유형 창 = 정의되지 않음(노드에서 정의되지 않음 => jsbn CSPRNG 분기 건너뛰기)
typeof globalThis.crypto = object(Node Web Crypto가 존재하지만 jsbn은 globalThis.crypto가 아니라 window.crypto를 확인함)
Math.random 샘플 = 0.704452488761137
Date.now() = 1784455686795(공격자 추정 가능, ARC4 시드에 혼합)
===== 결정적(Math.random + sm-crypto가 필요하기 전에 고정된 날짜) =====
--- #1 실행 --- PRIVATE=143268fa0939b4da09eab8c9a2e027a04555b6c433fef4f54fc5edd517c0a6b1
--- 실행 #2 --- PRIVATE=143268fa0939b4da09eab8c9a2e027a04555b6c433fef4f54fc5edd517c0a6b1
두 번의 결정론적 실행은 동일한 SM2 개인 키를 생성합니다.
키를 보여주는 것은 Math.random() + 벽시계 시간의 순수 함수입니다.
개인 키 복구/생성된 SM2 키 쌍의 서명 위조 Node.js의 기본 API입니다. 이는 Node.js의 가장 심각한 결함 클래스입니다. 유지 관리되는 SM2 라이브러리: 기본 키 생성 경로는 다음과 같습니다. 기본 런타임에서는 암호화되지 않습니다.
Math.random() + 벽시계 시간에서 파생됩니다. 할 수 있는 공격자
몇 가지 Math.random() 출력을 관찰하세요(V8 xorshift128+ 상태는
~4개의 관찰된 이중에서 복구 가능) 생성 시간을 추정할 수 있습니다.
개인 키를 복제하고 서명을 위조합니다.s = (k^-1)(e + d·r) mod n).Node.js의 CSPRNG에서 RNG를 시드합니다. sm-crypto의 가장 간단한 수정은 다음과 같습니다.
jsbn ARC4 인스턴스를 Web Crypto / crypto.randomBytes로 교체합니다.
``js // src/sm2/utils.js const nodeCrypto = (typeof require === 'function') ? require('암호화') : null; 함수 csrandBytes(n) { if (nodeCrypto) return nodeCrypto.randomBytes(n); // 노드 if (globalThis.crypto) { // 웹 암호화(브라우저/노드 ≥ 19) const b = 새로운 Uint8Array(n); globalThis.crypto.getRandomValues(b); b를 반환; } 새로운 오류를 발생시킵니다('사용 가능한 CSPRNG가 없습니다'); }
이를 사용하여 개인 키/임시 키를 직접 생성하거나
jsbn 풀. 별도의 (업스트림) 수정 사항이 jsbn에 속해 있는지 확인하세요.
`window.crypto` 외에 `globalThis.crypto`.
### 영향을 받는 버전
- npm `sm-crypto` **0.4.0**(최신, 게시일: 2026-01-20). 에 따라 다름
`jsbn ^1.1.0`(`jsbn@1.1.0`, `index.js` RNG가 근본 원인임).
- 런타임: Node.js(기본 런타임, 브라우저에서는 jsbn CSPRNG 분기)
촬영됩니다).
## 크레딧
diff/ambidiff 보안 연구 노력(afldl)에서 보고했습니다.
왜 이 VPI인가 (설명가능 · 실험적)
VPI 산정 기준
| 영향도 | 91.00 |
| 악용 신호(추가 악용신호 없음) | ×1.00 |
| VPI | 91.00 |
VPI 공식 vpi-v1 기준