[번외 6] 콜드카드 RNG 사태 — 하드웨어 난수가 시드 생성에서 빠진 이유
[번외 6] 콜드카드 RNG 사태 — 하드웨어 난수가 시드 생성에서 빠진 이유
원문: https://oasis-milk-790.notion.site/6-RNG-3a682e1163ca81adb2f5c62a594224f2
카테고리: 기타 🤖
💡
이 글은 Team Corn의 Berlin이 작성한 해설입니다. 2026년 7월 30일 공개된 콜드카드(COLDCARD)의 난수 생성기(RNG) 결함과 이후 온체인에서 잇달아 확인된 대규모 자금 이동(이하 ‘스윕’)을 다룹니다. 피해 규모와 조사 상태는 2026년 8월 2일까지 공개된 자료를 기준으로 정리했습니다.
>
작성자는 니모닉을 만들 때 가장 바람직한 방법은 사용자가 네트워크에서 분리된 상태에서 공정한 주사위나 동전으로 충분한 난수를 직접 만들고, 가능한 범위에서 계산 과정을 독립적으로 확인하는 것이라고 봅니다. 이는 특정 기기·제조사·개발자의 명성이나 ‘오픈소스’라는 사실에 키 생성을 전적으로 맡기지 않기 위한 선택입니다. 다만 물리 난수 입력도 절차 오류, 기록 노출, 악성 펌웨어와 주소 바꿔치기를 자동으로 막지는 못합니다. 따라서 서로 다른 장치와 절차로 만든 키를 사용하는 멀티시그와 별도의 주소 검증도 함께 권장합니다.
>
7장의 지갑 비교는 제품 순위표가 아닙니다. 각 제조사가 난수를 어떻게 만들고 섞는다고 설명하는지 공개 문서를 바탕으로 비교했습니다. 피해 규모, 세 스윕을 같은 공격자가 벌였는지, 실제 공격에 얼마나 많은 계산이 필요한지처럼 아직 확정되지 않은 부분은 확인된 사실과 연구진의 추정, 작성자의 해석을 나눠 적었습니다.
>
TL;DR
2026년 7월 30일 01:10–01:51 UTC에 벌어진 첫 번째 스윕에서는 1,196개 주소에서 1,082.65 BTC가 41분 만에 이동했다. 8월 2일까지 확인된 세 차례의 스윕을 합치면 4,585개 주소에서 1,367.05 BTC, 당시 시가 약 8,860만 달러가 이동했다. Galaxy Research는 각 스윕을 한 사람이 통제한 것으로 보았지만, 세 차례를 모두 같은 공격자가 벌였는지는 온체인 자료만으로 확정하지 못했다.
현재까지 확인된 원인은 기기 원격 침입이 아니었다. 새 코드를 넣는 과정에서 정상적으로 작동하던 난수 호출 경로가 깨졌다. 소프트웨어에서는 이런 문제를 회귀 오류(regression)라고 부른다. 2021년 3월 libNgU를 도입하는 과정에서 ngu.random이 STM32 하드웨어 RNG가 아니라 MicroPython의 결정론적 Yasmarang 소프트웨어 폴백(fallback)에 연결됐다. Mk2·Mk3의 취약 펌웨어에서는 시드 생성에 공격자가 예측할 수 없는 난수가 들어가지 않았다. 코인카이트는 특정 공격 조건에서 공격자가 뒤져야 할 후보 수가 약 2^40개에 해당한다고 추정한다. Mk4·Q·Mk5는 보안 칩(secure element, SE)에서 가져온 값으로 난수 생성기 상태를 한 번 더 초기화한다. 이를 재시딩이라고 하며, 제조사의 예비 추정치는 약 72비트다. Block의 2^32 계산은 공격자가 소프트웨어 폴백의 상태와 호출 이력을 이미 특정할 수 있다는 조건에서, SE 값을 섞은 뒤 나올 수 있는 난수열이 최대 2^32개라는 뜻이다. 40비트·72비트 추정치와 성격이 다른 수치이므로 서로 바꿔 쓰면 안 된다.
영향받은 펌웨어에서 시드를 만들었더라도, 공정한 주사위를 매번 따로 50회 이상 굴리고, 그 결과를 외부에 노출하지 않은 채 최종 시드에 추가했다면 코인카이트는 이 RNG 결함만으로는 위험하지 않다고 본다. 강하고 고유한 BIP-39 패스프레이즈도 추가 방어선이 되지만 취약한 시드를 안전한 시드로 바꾸지는 못하므로, 새 시드로 만든 지갑으로 자금을 옮기라는 공식 권고는 그대로 적용된다.
이 사건에서 얻을 수 있는 결론은 두 가지다. 사용자가 주사위나 동전으로 충분한 난수를 직접 만들어 넣으면, 기본 RNG 하나가 고장 나도 시드 전체가 함께 취약해질 위험을 줄일 수 있다. 서로 다른 장치와 절차로 각각 만든 세 키 가운데 두 개를 요구하는 2-of-3 멀티시그는 키 하나가 노출되더라도 지출에 필요한 두 번째 서명을 공격자가 확보하지 못하게 한다. 다만 주사위 입력은 악성 서명 펌웨어까지 막지 못하며, 취약한 콜드카드 여러 대의 키만으로 필요한 서명 수를 채우는 멀티시그도 안전하지 않다. 주사위는 난수 생성기의 실패에 대비하는 방법이다. 서로 다른 장치와 절차로 만든 키를 함께 쓰는 멀티시그는 키 하나가 뚫려도 곧바로 자금을 빼앗기지 않게 한다.
1. 2026년 7월 30일 — 41분 동안 1,082.65 BTC가 이동했다
초기 보도는 25분 동안 약 594 BTC가 이동했다고 전했고, 당시 확인 범위는 약 500개 단일서명 지갑이었다. 이후 Galaxy Research가 첫 번째 스윕 전체를 다시 추적하면서 규모가 거의 두 배로 늘었다. 7월 30일 01:10부터 01:51 UTC까지 41분 동안 1,196개 주소에서 1,082.65 BTC가 이동했다. 관련 거래는 여섯 개 블록에 나뉘어 포함됐고, 그 사이 세 개 블록에는 공격 관련 거래가 없었다. 세 블록 자체가 비어 있었다는 뜻은 아니다. 공격 거래가 연속해서 전파된 것이 아니라 여러 차례에 나눠 전파됐음을 시사한다. 첫 번째 스윕의 자금은 네 개의 수집 주소로 모였고, 8월 1일 공개된 분석 시점에는 움직이지 않은 상태였다. 초기 보도가 이 가운데 한 주소만 포착했기 때문에 총액에 큰 차이가 생겼다.
공격 순서도 무작위로 보이지 않았다. 첫 번째 스윕은 주소당 평균 잔액이 약 0.91 BTC였지만, 세 번째 스윕은 주소당 평균 약 0.11 BTC의 더 작은 잔액을 겨냥했다. 이는 공격자가 후보 시드에서 파생한 주소와 잔액을 사전에 계산하고, 잔액이 큰 주소부터 처리했을 가능성과 일치한다. 다만 이는 온체인 순서와 잔액 분포에서 나온 해석이다. 공격자가 어떤 오프체인 자료를 보유했는지, 세 스윕을 같은 운영자가 수행했는지는 온체인 자료만으로 확정할 수 없다.
여기서 온체인 분석의 한계를 분명히 해야 한다. 블록체인에는 시드를 만든 날짜나 사용한 콜드카드 모델·펌웨어가 기록되지 않는다. 주소의 최초 입금 시점과 UTXO의 연령은 확인할 수 있지만, 그것이 곧 시드 생성 시점은 아니다. 따라서 “모든 피해 시드가 2021년 3월 17일 이후 생성됐다”는 문장은 온체인에서 직접 확인된 사실로 쓸 수 없다. 공개된 코드 분석과 피해자 보고, 온체인 패턴은 취약 경로에서 만들어진 키가 도난에 이용됐다는 설명과 일치한다. 그러나 주소별 기기 모델과 생성 펌웨어는 온체인 자료만으로 직접 확인되지 않는다.
코인카이트의 대응 범위도 조사와 함께 넓어졌다. 7월 30일 Mk2·Mk3 보안 권고가 나왔고, 이후 Mk4·Mk5·Q와 Standard·Edge 릴리스까지 포함한 공식 권고와 기술 분석이 갱신됐다. 8월 2일 Galaxy 집계에 따르면 세 차례의 스윕에서 누적 1,367.05 BTC, 약 8,860만 달러가 4,585개 주소에서 이동했다. 세 번째 스윕은 1,912개 주소에서 약 208 BTC를 이동시키며 더 작은 잔액을 겨냥했고, 각 피해 주소의 자금을 별도의 P2WSH 출력으로 보냈다. Galaxy는 각 스윕을 한 사람이 통제한 것으로 보았지만, 세 차례를 모두 같은 공격자가 벌였는지는 확인하지 못했다. 자금이 바로 다시 이동하지 않은 이유 역시 공개 자료만으로는 알 수 없다. 이 글의 피해 수치는 2026년 8월 2일 기준이며 이후 달라질 수 있다.
2. 원인 — 시드 생성 코드가 하드웨어 난수 대신 소프트웨어 PRNG를 사용했다
콜드카드에는 STM32의 하드웨어 난수를 읽는 ckcc.rng_bytes()가 있었고, 이 기능 자체는 정상으로 남아 있었다. 문제는 2021년 3월 시드 생성 코드를 ngu.random.bytes()로 바꾸면서 시작됐다. 이 함수가 호출한 rng_get()이 하드웨어 구현이 아니라 MicroPython의 소프트웨어 폴백에 연결됐다. 즉, TRNG가 고장 난 것이 아니라 시드를 만드는 코드가 TRNG를 사용하지 않은 것이다. 코인카이트의 기술적 사후 분석과 Block의 독립 코드 분석은 이 점에서 같은 결론을 내린다.
빌드 단계의 안전장치도 이 문제를 잡아내지 못했다. 이유는 전처리기 조건문에 있었다. 보드 설정은 보드 전용 난수 구현을 사용하기 위해 MICROPY_HW_ENABLE_RNG를 0으로 정의했다. 그러나 libNgU의 안전장치는 이 매크로의 값이 0인지가 아니라, 매크로가 정의돼 있는지만 검사했다.
#ifndef MICROPY_HW_ENABLE_RNG
#error "get a HW TRNG plz" ← TRNG 없으면 빌드 중단(의도)
#endif
#ifndef는 매크로의 값이 0인지 확인하지 않는다. MICROPY_HW_ENABLE_RNG가 0으로 정의돼 있어도 매크로는 “정의된 상태”이므로 해당 오류 블록에 들어가지 않고 빌드가 계속된다. 동시에 보드 전용 구현은 전역 함수 rng_get()이 아니라 random32()와 random_buffer()를 내보냈다. 그 결과 링커는 libNgU가 참조한 rng_get()을 MicroPython의 폴백 구현에 연결했다. 이것은 흔히 말하는 “한 글자 오타”가 아니었다. 매크로가 존재하는지와 그 값이 무엇인지를 혼동한 검사에, 같은 함수 이름을 가진 소프트웨어 폴백이 맞물리면서 생긴 오류였다.
MicroPython은 이 설정값이 0일 때 rng_get()을 제거하지 않고 Yasmarang 소프트웨어 폴백을 컴파일한다.
#if MICROPY_HW_ENABLE_RNG
uint32_t rng_get(void) { ... return RNG->DR; } // 실제 물리 잡음
#else
uint32_t rng_get(void) { return pyb_rng_yasmarang(); } // 소프트웨어 PRNG
#endif
libNgU는 rng_get()의 출력과 자체 Yasmarang 상태를 XOR로 결합했다. 의도대로라면 전자는 칩의 하드웨어 난수여야 했다. 실제로는 MicroPython의 Yasmarang 폴백이었고, libNgU의 두 번째 Yasmarang은 공개 상수에서 시작했다. 결국 Mk2·Mk3는 재현 가능한 비암호학적 PRNG 두 개의 출력을 XOR해 시드를 만들었다. XOR는 두 입력에 없던 난수를 새로 만들어내지 못한다. 따라서 이 경로에는 공격자가 예측할 수 없는 난수가 남지 않았다.
MicroPython의 Yasmarang은 UID_low32 XOR SysTick, 실시간 시계 값(RTC->TR)과 1초 미만 카운터(RTC->SSR)로 초기 상태를 만들었다. UID는 기기마다 고정된 식별 정보이고, SysTick과 RTC는 공격자가 관찰하거나 범위를 좁힐 수 있는 시각·타이머 값이다. 이 값들은 서로 독립적으로 얻은 암호학적 난수가 아니다. 이 값들과 시드를 만들기 전까지 RNG를 몇 번 호출했는지가 정해지면 이후 난수열도 결정된다. 공격자는 후보 상태마다 니모닉·키·주소를 파생해 공개 블록체인과 대조할 수 있다. 다만 실제 후보 수는 UID, 부팅 시점, RTC 상태와 선행 호출 이력을 공격자가 얼마나 좁힐 수 있는지에 따라 달라진다. 따라서 하나의 수치로 모든 기기의 공격 비용을 표현해서는 안 된다.
코인카이트는 현재 제시한 공격 조건에서 Mk2·Mk3의 후보 수가 약 2^40개에 해당한다고 추정한다. 공식 보안 권고에서 명시한 영향 범위는 4.0.1–4.1.9이다. Block은 취약 코드가 2021년 3월 17일의 v4.0.0 태그부터 존재했다고 추적했지만, Wizardsardine의 독립 분석에 따르면 v4.0.0은 서명된 배포 목록에 없었고 일반 사용자에게 공개 배포되지 않았다. 따라서 코드상 최초 영향 버전은 v4.0.0, 확인된 첫 공개 배포본은 v4.0.1로 구분하는 편이 정확하다. 직접 빌드했거나 비공식 v4.0.0을 사용한 경우에만 v4.0.0도 영향 대상으로 본다. 3.2.2 이하에서 만든 시드는 기존 ckcc.rng_bytes() 경로를 사용했고, Mk2·Mk3 4.x 계열에서는 4.2.0부터 새 시드 생성 경로가 수정됐다. 같은 Wizardsardine 분석은 Mk3 5.0.1-mk3·5.0.3-mk3도 약 40비트로 분류하지만, 현재 코인카이트 공식 권고에는 이 두 버전이 없다. 해당 버전에서 시드·키·비밀번호를 만들었다면 제조사가 범위를 추가로 확인하기 전까지는 영향을 받은 것으로 보고 새 지갑으로 자금을 옮기는 편이 안전하다.
Mk4·Q·Mk5는 부팅할 때 SE1과 SE2에서 값을 받아 해시한 뒤, 그 결과로 libNgU의 난수 생성기 상태를 한 번 더 초기화한다. Block 분석에 따르면 SE1은 공격자가 예측하기 어렵고 인증된 32바이트 값을 반환하지만, SE2의 8바이트는 실시간 난수 명령이 아니라 ROM 옵션 페이지에서 읽힌다. 두 값을 SHA256d로 섞지만, 그 결과에서 4바이트만 reseed()에 넘긴다. reseed()는 Yasmarang의 32비트 상태 변수 하나만 바꾼다. 코인카이트는 이 추가 입력을 반영해 신형 모델의 후보 수가 약 2^72개에 해당한다고 추정한다. Block은 공격자가 기기 상태를 얼마나 알고 있는지에 따라 계산을 다시 나눴다. 공격자가 MicroPython 폴백 상태와 RNG 호출 이력을 이미 특정할 수 있다면, SE 값을 섞은 뒤 나올 수 있는 난수열은 최대 2^32개이며 평균적으로 약 2^31개를 시험하면 된다. 반대로 공격자가 타이머 상태를 거의 모른다고 가정해 후보를 가장 넓게 잡아도 2^73.3개 미만이다. 이 계산이 73비트의 서로 독립적인 비밀 난수가 들어 있다는 뜻은 아니다. 타이머 값들이 서로 연관돼 있고, 공격자가 이를 관찰하거나 다시 추정할 수 있기 때문이다. 따라서 신형 모델을 일률적으로 “32비트라서 43초면 해독된다”고 쓰는 것도, 반대로 “72비트이므로 현실적으로 안전하다”고 쓰는 것도 부정확하다. 분명한 것은 의도했던 128비트에 미치지 못했고, 공격자가 기기 상태를 많이 알아낼수록 확인해야 할 후보가 급격히 줄어든다는 점이다. Block은 아직 시드 생성부터 주소 대조까지 전 과정을 포함한 전수탐색 성능 측정치를 제시하지 않았다. 또한 초기화 도중 예외가 발생해도 재시딩 없이 계속될 수 있는 코드 경로를 지적했다. 다만 일반 양산 기기에서는 SE 통신이나 인증에 실패하면 부트로더가 멈추는 것으로 보이므로, 이 경로가 실제 장치에서 아무 경고 없이 이어지는지는 확인되지 않았다. 이 경우는 32비트 재시딩이 정상적으로 이뤄진 경우와 구분해야 한다.
그렇다면 왜 약 5년간 발견되지 않았는가. 세 가지 요인이 겹쳤다.
첫째, 해시 처리가 약한 입력을 무작위처럼 보이게 했다. 시드 생성 경로는 ngu.random.bytes(32)가 낸 32바이트를 SHA256d(이중 SHA-256)로 처리했다. 암호학적 해시는 편향된 입력을 균일해 보이는 출력으로 바꿀 수 있지만, 입력에 없던 엔트로피를 추가하지는 못한다. 후보 입력이 2^40개라면 해시 출력도 최대 2^40개 후보 안에 있다. 그래서 완성된 니모닉 하나만 보고 그것이 충분한 난수로 만들어졌는지, 가능한 내부 상태가 매우 적은 생성기에서 나왔는지를 판별하기는 어렵다. 겉보기에 무작위인 것과 공격자가 같은 값을 다시 만들어낼 수 없는 것은 같은 뜻이 아니다.
둘째, 결함이 여러 저장소와 모듈 사이에 걸쳐 있었다. 2021년 3월 1일 커밋 b18723dd는 새 시드 생성을 rng_bytes(seed)에서 random.bytes(32)로 바꿨고, 이 호출은 ngu.random.bytes()로 이어졌다. 보드 전용 STM32 RNG 구현은 최종 바이너리에 남아 있었기 때문에 그 코드만 확인하면 정상처럼 보였다. 실제 문제는 펌웨어, libNgU, MicroPython 포크를 함께 빌드했을 때 어느 함수가 최종적으로 연결되고 실제로 호출되는지에 있었다. 영향도 시드 생성에만 한정되지 않았다. ngu.random을 사용한 페이퍼 월렛 키, 임의 Seed XOR 마스크, 일부 복제·USB·Key Teleport·Web2FA 키, Secure Notes의 자동 생성 비밀번호와 HSM 로컬 코드도 같은 약한 난수의 영향을 받았다. 반대로 ckcc.rng_bytes()를 직접 사용한 기능은 별도의 STM32 하드웨어 RNG 경로를 유지했다.
셋째, 소스 코드 공개와 실제 보안 검증은 같은 말이 아니다. 소스가 공개돼 있다는 사실은 검증할 기회를 제공할 뿐, 누군가 필요한 범위까지 실제로 검증했다는 뜻은 아니다. 무엇을 검토했는지, 빌드 결과에서 어느 함수가 실제로 연결됐는지, 시드 생성 과정을 처음부터 끝까지 시험했는지가 분명해야 한다. 코인카이트의 사후 설명에 따르면 기존 검토는 의도한 TRNG 코드가 바이너리에 포함됐는지는 확인했지만, 지갑 생성 호출이 libNgU와 MicroPython을 거쳐 실제로 어느 rng_get()에 도달하는지는 검증하지 못했다. 이번 수정에는 MicroPython 폴백 코드를 명시적으로 제외하고, 보드 전용 rng_get() 함수가 없으면 빌드를 실패시키는 검사도 포함됐다.
오픈소스는 안전 보증서도, 실수에 대한 면책 사유도 아니다. 이번 사건은 숙련된 개발자와 이름난 팀도 “하드웨어 난수가 존재하는가”보다 더 중요한 기본 질문, 즉 그 난수가 실제 시드 생성 경로에 들어가는가를 놓칠 수 있음을 보여준다. 복잡한 저장소와 빌드 과정, 모듈 사이에서는 작은 실수가 수년간 숨어 있을 수 있다. 사용자가 코드를 읽을 능력이 있더라도 모든 저장소와 빌드 설정, 최종 함수 연결, 하드웨어 호출을 끝까지 확인하기는 쉽지 않다. 자신의 검증 능력 역시 과신해서는 안 된다.
교훈은 “오픈소스는 소용없다”가 아니다. 오픈소스는 검증을 가능하게 하지만 검증을 대신하지 않는다. 특정 기기·제조사·개발자의 명성만으로 키 생성을 맡겨서는 안 된다. 직접 확인할 수 있는 부분은 확인하고, 확인하기 어려운 부분은 서로 독립적인 키와 멀티시그로 실패가 자금 손실까지 번지는 것을 막아야 한다.
코드가 돌아간다고 난수가 안전한 것은 아니다
Bitcoin++ Insider에 실린 Core Lightning 개발자 Dusty Daemon의 기고문은 이 사건을 커밋 기록과 코드 리뷰 과정에서 다시 살펴본다. 필자가 주목한 콜드카드 펌웨어 커밋 37e4af5는 Mk4용 MicroPython 포트를 추가하면서 17개 파일에 1,534줄을 넣었지만, 커밋 메시지는 runs 한 단어였다. libNgU 커밋 f19de05는 28개 파일에서 881줄을 추가하고 200줄을 삭제했지만 메시지는 x 한 글자였다. 둘 다 난수 처리와 암호 라이브러리의 연결에 영향을 줄 수 있는 큰 변경이었지만, 커밋 기록만으로는 왜 바꿨는지, 어떤 함수가 실제로 호출돼야 하는지, 무엇을 시험했는지 알 수 없다.
여기서 중요한 것은 커밋 메시지의 길이 자체가 아니다. 보안에 중요한 변경의 의도와 반드시 지켜야 할 조건이 기록되지 않았다는 점이다. 지갑 생성 함수에서 random.bytes(32)는 오류 없이 32바이트를 반환했고, 바이트 종류가 너무 적지 않은지 확인하는 assert len(set(seed)) > 4도 통과할 수 있었다. 화면에는 정상적인 12·24단어가 나타났고 펌웨어도 실행됐다. 그러나 이 검사는 출력이 지나치게 단조로운지만 확인할 뿐, 그 값에 공격자가 예측할 수 없는 난수가 들어갔는지는 확인하지 못한다. 함수가 값을 반환하고 프로그램이 정상 실행됐다는 사실은 그 난수가 안전하다는 증거가 아니다.
이런 코드는 변경 범위를 작게 나누고, 커밋 메시지에 최소한 다음 내용을 남겨야 한다. 시드 생성이 반드시 어느 하드웨어 난수 함수를 거쳐야 하는지, 소프트웨어 폴백이 연결되면 왜 빌드가 실패해야 하는지, 최종 바이너리에서 실제 호출 경로를 어떻게 확인했는지, 하드웨어 난수를 끊었을 때 시험이 반드시 실패하는지를 기록해야 한다. 출력 길이와 겉보기 분포만 검사해서는 안 되며, 최종 빌드에서 올바른 함수가 연결됐는지와 시드 생성까지 실제로 도달하는지를 자동 시험해야 한다.
다만 이 기고문의 모든 해석을 확정된 사실로 받아들여서는 안 된다. 필자는 중복 함수 정의로 생긴 컴파일 오류를 피하려고 MICROPY_HW_ENABLE_RNG를 0으로 바꿨을 가능성을 제시하고, 당시 개발자의 판단 과정도 강하게 비판한다. 그러나 공개된 커밋 기록만으로 개발자의 의도나 정확한 디버깅 순서까지 확정할 수는 없다. 이 글에서는 확인 가능한 커밋 메시지와 변경 규모, 실제 호출 경로, 그리고 코드 리뷰에 남긴 교훈만 받아들인다. 기술적 원인은 코인카이트·Block·Wizardsardine의 코드 분석과 교차 확인한 기존 설명을 유지한다.
시드 생성 경로 비교
| 단계 | 의도된 동작 | 취약 펌웨어에서 실제로 일어난 일 |
|---|---|---|
| 새 시드 요청 | ngu.random.bytes(32)가 안전한 난수 32바이트를 요청 | 호출은 오류 없이 실행되고 32바이트를 반환 |
rng_get() 연결 | STM32 하드웨어 RNG에서 난수를 읽음 | MicroPython의 결정론적 Yasmarang 폴백을 호출 |
| libNgU 내부 처리 | 하드웨어 난수와 내부 상태를 XOR | 재현 가능한 Yasmarang 출력 두 개를 XOR |
| 결과 | 공격자가 예측하기 어려운 시드 | 제한된 내부 상태를 열거해 후보를 다시 만들 수 있는 시드 |
TRNG와 PRNG — 정확히 무엇이 실패했나
이번 결함을 한 문장으로 정리하면 이렇다. 시드 생성 코드가 암호학적으로 안전한 난수 대신 비암호학적 PRNG의 출력을 받았다. 여기서는 비암호학적 PRNG, 암호학적으로 안전한 의사난수 생성기(CSPRNG)·DRBG, 그리고 물리 난수 생성기(TRNG)를 구분해야 한다.
PRNG(Pseudo-Random Number Generator, 의사난수 생성기)는 초기 상태에서 결정론적으로 수열을 만든다. 그러나 모든 PRNG가 암호 용도에 부적합한 것은 아니다. 충분한 비밀 난수로 초기화되고, 일부 출력을 보더라도 다음 값을 쉽게 예측하지 못하도록 설계된 CSPRNG와 DRBG는 현대 암호 시스템에서 널리 사용된다. 이번 사건의 Yasmarang과 Milk Sad의 MT19937은 그 부류가 아니다. 통계적으로 무작위처럼 보이는 출력을 만들 수는 있어도, 작은 상태 공간이나 관측된 상태에서 출력이 재현될 수 있는 비암호학적 PRNG다.
TRNG(True Random Number Generator, 물리 난수 생성기)는 열잡음·아발란체 잡음·클록 지터 같은 물리 현상을 측정해 예측하기 어려운 비트를 얻는다. 다만 TRNG를 탑재했다고 자동으로 안전해지는 것은 아니다. 편향 제거와 상태 검사, 고장을 감지하면 안전하게 중단하는 동작, 올바른 드라이버 연결이 필요하다. 보통 TRNG가 처음 난수를 공급하고, CSPRNG가 이를 바탕으로 필요한 만큼의 난수를 만든다. 문제는 하드웨어 난수 생성기 자체가 아니었다. 시드 생성 코드가 그 난수를 실제로 사용하지 않았다는 데 있었다.
따라서 하드웨어 지갑을 고를 때는 다음을 확인해야 한다. 사양표에 ‘TRNG 탑재’라고 적혀 있는 것만으로는 부족하다. 실제로 시드를 만드는 코드가 어떤 난수원을 사용하는지, 난수원에 문제가 생기면 안전하게 멈추는지, 그 과정을 별도로 시험할 수 있는지를 확인해야 한다. 사용자가 주사위처럼 충분한 난수를 직접 만들어 넣고 공개된 계산법으로 결과를 대조하면 기기 기본 RNG에 대한 의존을 줄일 수 있다. 다만 악성 펌웨어가 입력을 훔치거나 최종 키를 바꿀 가능성까지 제거하지는 못한다.
여러 난수원을 함께 사용하는 것도 좋은 방어책이 될 수 있다. 다만 값을 올바르게 섞어야 하고, 최소 하나는 공격자가 예측할 수 없어야 하며, 난수원 하나가 고장 나면 이를 감지하고 멈출 수 있어야 한다. 난수원이 여러 개라는 사실만으로 보안이 증명되는 것은 아니다. 7장에서는 이 조건을 기준으로 지갑별 공개 설계를 비교한다.
3. 엔트로피가 낮다는 것은 무엇을 뜻하는가
n비트 난수가 모든 값에 같은 확률로 뽑힌다면 가능한 값은 2^n개다. 그러나 실제 장치의 난수가 치우쳐 있거나 공격자가 기기 정보와 시각 정보를 알고 있다면, 일부 후보부터 먼저 시험할 수 있다. 따라서 출력이 몇 비트로 보이느냐보다 공격자가 실제 후보를 얼마나 빨리 줄일 수 있느냐가 중요하다. 이 글에서 말하는 “유효 엔트로피”와 “탐색 공간”은 특정 공격 조건에서 계산한 근사치다. 모든 기기에 언제나 똑같이 적용되는 고정값은 아니다.
32비트 ≈ 4.3 × 10⁹ — 특정 조건에서 SE 재시딩 뒤 가능한 난수열의 최대치
40비트 ≈ 1.1 × 10¹² — 코인카이트의 Mk2·Mk3 예비 추정치
72비트 ≈ 4.7 × 10²¹ — 코인카이트의 Mk4·Q·Mk5 예비 추정치
128비트 ≈ 3.4 × 10³⁸ — BIP-39 12단어에 들어가는 난수
256비트 ≈ 1.2 × 10⁷⁷ — BIP-39 24단어에 들어가는 난수
여기서 32·40·72비트는 같은 종류의 숫자가 아니다. 40비트와 72비트는 제조사가 특정 공격 가정 아래 제시한 예비 추정치다. 반면 2^32는 공격자가 신형 모델의 폴백 상태와 호출 이력을 이미 특정했을 때, SE 재시딩 뒤 가능한 난수열이 최대 2^32개라는 계산이다. 이를 서로 바꿔 쓰거나 단순 비교하면 안 된다.
절대 시간보다 먼저 후보 수의 비율을 보는 편이 정확하다. 2^40은 2^32보다 256배 크고, 2^72는 2^40보다 약 43억 배 크다. 그러나 이 비율만으로 실제 공격 시간을 계산할 수는 없다. 후보가 될 내부 상태를 다시 만들고, BIP-39와 PBKDF2-HMAC-SHA512 2,048회, BIP-32 키 파생, 주소 계산과 온체인 대조까지 수행해야 하기 때문이다. 실제 속도는 프로그램과 하드웨어, 공격자가 이미 알고 있는 정보에 따라 달라진다. Block도 후보 생성부터 주소 대조까지 포함한 실제 전수탐색 속도는 제시하지 않았다.
따라서 72비트와 32비트를 같은 “몇 초짜리 공격”으로 묶는 것은 수학적으로 틀리다. 더 중요한 점은 제조사의 72비트 예비 추정치가 72비트의 독립적인 비밀 엔트로피를 뜻하지 않는다는 것이다. 공격자가 UID·타이밍·호출 이력을 알아낼수록 실제로 남는 후보 수는 훨씬 줄어들 수 있다.
실무에서 128비트를 기준으로 삼는 이유는 현재의 일반 컴퓨터로 전수탐색하기가 현실적으로 불가능하고, secp256k1도 일반적인 이산로그 공격에 대해 약 128비트 수준의 보안을 제공하기 때문이다. 균등한 128비트 난수로 만든 12단어 니모닉이면 이 보안 수준에 맞는다. 24단어는 256비트 난수와 더 긴 체크섬을 사용하지만, 비트코인 서명키 자체의 보안을 256비트로 높여 주는 것은 아니다. 24단어는 더 큰 난수 공간을 사용하므로, 장기 보관에서 더 여유를 두려는 사용자가 선택할 수 있다.
사람이 머릿속에서 고른 숫자나 문장은 물리 난수가 아니다. 브레인월렛이 반복해서 사전 공격에 무너진 이유도 인간의 선택이 강하게 편향되고 재사용되기 때문이다. 사용자가 직접 난수를 만들려면 주사위나 동전처럼 경우의 수와 필요한 횟수를 계산할 수 있는 물리적 방법을 써야 한다. 기록 유출, 부족한 횟수, 입력 실수도 함께 관리해야 한다. 6장에서 그 절차와 한계를 구분한다.
4. BIP-39 — 12·24개 단어는 어떻게 만들어지는가
지갑에 표시되는 12개나 24개의 단어는 임의로 고른 단어가 아니다. 기기가 먼저 난수를 만들고, 잘못 기록했는지 확인하기 위한 체크섬을 덧붙인 뒤, 전체 비트열을 BIP-39 단어 목록의 단어로 바꾼다. 이 단어 묶음만 있으면 지갑을 복구할 수 있으므로 반드시 비밀로 보관해야 한다.
BIP-39는 이 과정을 다음과 같이 정한다. 먼저 128–256비트의 난수 ENT를 준비한다. ENT를 SHA-256으로 해시한 뒤, 해시의 앞 ENT/32비트를 체크섬 CS로 붙인다. 이어 붙인 ENT‖CS를 11비트씩 잘라 각 숫자를 2,048개 단어 목록의 순번으로 바꾼다. 128비트 난수는 4비트 체크섬과 합쳐 132비트가 되므로 12단어가 된다. 256비트 난수는 8비트 체크섬과 합쳐 264비트, 즉 24단어가 된다. 15·18·21단어도 같은 방식으로 만든다.
여기서 중요한 점은 두 가지다.
첫째, 단어 수가 같다고 안전성까지 같은 것은 아니다. 가능한 내부 상태가 적은 생성기에서 나온 12단어와 균등한 128비트 난수에서 나온 12단어는 겉모양도 같고 체크섬도 정상이다. 체크섬은 단어를 잘못 적었는지 일부 확인할 뿐, 처음 사용한 난수가 충분히 강했는지는 알려주지 않는다. BIP-39는 난수를 단어로 바꾸고, 그 단어에서 지갑 시드를 만드는 방법까지만 정한다. 하드웨어 지갑이 어떤 난수원을 쓰고 여러 값을 어떻게 섞어야 하는지는 정하지 않는다. Trust Wallet, Milk Sad, Ill Bloom, 콜드카드 사건은 모두 같은 사실을 보여준다. 난수를 만드는 구현이 약하면 형식이 올바른 니모닉도 안전하지 않다.
둘째, 마지막 단어의 일부는 체크섬이다. 12단어 니모닉의 마지막 단어에는 난수 7비트와 체크섬 4비트가 들어간다. 24단어에서는 난수 3비트와 체크섬 8비트가 들어간다. 단어 목록에서 임의로 12개를 고르면 16번 중 한 번, 24개를 고르면 256번 중 한 번만 체크섬이 맞는다. 따라서 주사위 결과로 니모닉을 직접 만들 때는 해시를 계산해 올바른 마지막 단어를 찾아야 한다. 장치의 ‘마지막 단어 계산’ 기능이나 검증된 오프라인 도구를 사용할 수 있지만, 실제 주사위 결과를 입력하는 환경도 하드웨어 지갑만큼 신뢰할 수 있어야 한다.
니모닉을 지갑이 실제로 사용하는 시드로 바꿀 때는 PBKDF2-HMAC-SHA512 계산을 2,048회 반복한다. 니모닉과 패스프레이즈를 각각 NFKD로 정규화하고 UTF-8로 인코딩하며, 솔트에는 "mnemonic" + passphrase를 사용한다. 그 결과 512비트가 나오고, 이를 BIP-32 마스터 키를 만드는 데 쓴다. 같은 니모닉이라도 패스프레이즈가 다르면 완전히 다른 지갑이 만들어진다.
그러나 패스프레이즈를 썼다는 사실만으로 안전해지는 것은 아니다. 충분히 길고 무작위인지, 다른 곳에 재사용하지 않았는지, 외부에 노출되지 않았는지가 중요하다. 짧은 단어, 인용문, 재사용 암호, PIN 수준 문자열은 반복 횟수가 2,048회로 비교적 낮아 많은 후보를 빠르게 시험하는 공격에 취약하다. 반대로 충분히 길고 무작위하며 다른 곳에 쓰지 않은 패스프레이즈는 약한 니모닉이 복구된 뒤에도 강한 추가 방어선이 된다. 어떤 문자열을 입력해도 유효한 지갑이 만들어지므로, 오타가 나도 경고가 뜨지 않는다. 새 지갑에 입금하거나 자금을 옮기기 전에는 지갑 지문과 수신 주소를 반드시 대조해야 한다.
5. 겉보기만으로는 알 수 없다 — 약한 시드 사고가 반복되는 이유
사용자에게 시드 생성은 대개 버튼 하나와 12·24개 단어로 보인다. 완성된 니모닉 하나만 보고 충분한 난수로 만들었는지, 가능한 내부 상태가 매우 적은 생성기에서 나왔는지를 거꾸로 알아내기는 어렵다. 체크섬이 맞고 단어가 무작위처럼 보여도, 시드를 만들 때 충분한 난수를 사용했다는 보장은 없다.
실패 원인은 우발적인 구현 결함일 수도 있고, 제조사·공급망·악성 펌웨어가 공격자만 재현할 수 있도록 키 생성 과정에 백도어를 심은 클렙토그래피(kleptography)일 수도 있다. 출력만 보는 사용자는 두 경우를 구분하기 어렵다. 따라서 완성된 단어의 겉모습이 아니라, 어떤 난수를 넣었는지, 여러 값을 어떻게 섞었는지, 최종 빌드에서 어느 함수가 호출됐는지, 실제 시드가 어떤 과정을 거쳐 만들어졌는지를 확인해야 한다.
이 유형의 사고는 반복돼 왔다.
2013 Android SecureRandom 결함
— Android에서 생성한 키와 ECDSA 서명 난수의 안전성 훼손, 키 교체 권고
2017–2023 Milk Sad(CVE-2023-39910)
— Libbitcoin Explorer `bx seed`, MT19937·32비트 내부 엔트로피
2021–2026 콜드카드 난수 호출 경로 오류
— Mk2·Mk3 약 40비트 추정, Mk4·Q·Mk5 약 72비트 추정
2022–2023 Trust Wallet 브라우저 확장(CVE-2023-31290)
— MT19937을 32비트 값으로 초기화, 약 43억 개 니모닉 공간
2026 Ill Bloom
— 특정 조건의 일부 모바일 소프트웨어 지갑에서 약한 PRNG 확인
이 가운데 Milk Sad는 구조가 특히 비슷하다. Libbitcoin Explorer 3.0.0–3.6.0의 bx seed는 MT19937을 사용해 설정과 무관하게 내부 엔트로피가 32비트로 제한됐다. 형식상 정상인 긴 시드를 출력해도 실제 후보는 약 43억 개뿐이었다. NVD는 이 결함이 2023년 6월과 7월 실제 공격에 이용됐다고 기록한다. 콜드카드와 마찬가지로 출력 길이와 체크섬은 정상이었지만, 생성기가 가질 수 있는 상태가 너무 적어 공격자가 후보 키를 미리 만들고 공개 주소와 대조할 수 있었다.
2013년 Android 사건은 RNG 실패가 키 생성과 서명 모두를 위협한다는 오래된 사례다. Android의 보안 난수 구현 결함 때문에 여러 지갑 앱에서 만든 키가 위험해졌고, ECDSA 서명 난수(nonce)의 재사용·예측 가능성 문제도 함께 제기됐다. bitcoin.org의 보안 경보는 업데이트만으로 기존 키가 회복되지 않으므로, 수정된 생성기로 새 주소를 만들고 자금을 옮기는 키 교체(key rotation)를 권고했다. 콜드카드와 마찬가지로, 패치만 해서는 기존 키가 안전해지지 않았다.
Ill Bloom은 2026년에도 같은 계열의 실패가 이어졌음을 보여준다. Coinspect는 특정 조건에서 일부 복구 구문이 약한 PRNG로 생성됐고, 2026년 5월 27일부터 온체인 악용 흔적이 확인됐다고 밝혔다. 다만 현재 공개 범위는 제한적이다. 연구진은 하드웨어 지갑에서 생성한 시드는 이 문제의 영향권 밖이라고 보고 있으며, 현재 가장 유력하게 지목되는 것은 널리 쓰이지 않은 일부 모바일 소프트웨어 지갑이다. 공개 주소만으로 원래 사용한 앱을 확정할 수 없고, 데이터셋도 완전하지 않다. 따라서 콜드카드 사건과 공통된 약한 엔트로피 문제로 비교할 수는 있지만, 피해 범위와 원인을 동일하게 취급해서는 안 된다.
공통점은 세 가지다. 첫째, 출력 형식과 길이가 정상이어도 시드를 만들 때 사용한 난수는 약할 수 있다. 둘째, 취약한 시드를 다른 앱이나 새 기기로 가져가도 강해지지 않는다. 셋째, 패치 이후에는 새 시드와 키를 만들고 자금을 이전해야 하며, 기존 비밀을 재사용해서는 안 된다.
누가 무엇을 확인할 수 있는지도 구분해야 한다. 개발자는 같은 소스에서 같은 바이너리가 나오는지 확인하고, 실제로 연결되는 함수와 호출 순서를 추적하며, 난수원 상태 검사와 테스트 벡터를 사용할 수 있다. 사용자는 공개된 구현과 감사 결과를 검토하고, 서로 다른 난수원을 결합한 제품을 고르거나 충분한 주사위·동전 입력을 직접 넣어 최소 하나의 난수원을 스스로 통제할 수 있다. 사용자가 주사위나 동전으로 난수를 직접 넣는 것은 강력한 방어책이다. 그러나 이것만이 유일한 검증 방법은 아니며, 기기 자체가 악성일 가능성까지 없애 주지는 않는다.
6. 주사위와 동전 — 직접 만들고, 직접 검증한다
필요한 횟수 계산
공정한 6면체 주사위를 매번 따로 굴리면 한 번에 최대 log₂6 ≈ 2.585비트, 공정한 동전을 던지면 한 번에 1비트의 엔트로피를 얻을 수 있다. 따라서 이상적인 조건에서 128비트를 확보하려면 주사위 50회(≈129.25비트) 또는 동전 128회가 필요하다. 주사위 99회는 ≈255.91비트로 256비트에 약간 못 미치고, 100회는 ≈258.50비트의 입력 엔트로피를 제공한다. 다만 주사위 문자열을 SHA-256으로 압축하면 최종 출력의 엔트로피 상한은 256비트다. 콜드카드 문서는 실무 기준으로 50회를 128비트, 99회를 “약 256비트”라고 안내한다. 따라서 이 글에서는 콜드카드 문서의 기준은 50회·99회, 이상적인 주사위 입력이 수학적으로 256비트를 넘는 기준은 100회, 최종 SHA-256 출력의 상한은 256비트로 구분한다.
횟수만 채운다고 끝나는 것도 아니다. SHA-256은 입력을 균일해 보이는 256비트 문자열로 바꿀 수 있지만, 부족하거나 예측 가능한 난수를 더 강한 난수로 만들어 주지는 못한다. 이상적인 주사위를 15회 던져도 엔트로피는 최대 약 38.8비트뿐이고, 특정 면이 자주 나오거나 사용자가 결과를 다시 굴리면 실제 엔트로피는 더 낮다. 정밀한 카지노용 주사위가 반드시 필요한 것은 아니지만, 눈에 띄게 손상된 주사위는 피한다. 매번 충분히 흔들어 던지고, 마음에 들지 않는 결과를 선택적으로 버리지 않아야 한다. 해시는 입력을 압축·정제할 뿐, 엔트로피를 새로 만들어내지 않는다.
콜드카드에서 주사위를 쓰는 세 가지 방법
콜드카드에서 시드를 만드는 방법은 세 가지로 나뉜다. 첫째는 기기 RNG만으로 새 시드를 만드는 기본 경로다. 둘째는 이미 만든 기기 시드와 사용자의 주사위 입력을 함께 해시하는 Add Dice Rolls다. 셋째는 New Seed Words > Advanced의 12/24 Word Dice Roll로, 주사위 숫자의 ASCII 문자열을 SHA-256해 시드에 사용할 값을 만드는 방식이다.
이번 결함에서 둘째와 셋째가 중요했던 이유는 사용자가 직접 넣은 주사위 결과가 최종 시드에 실제로 반영됐기 때문이다. Add Dice Rolls에서는 기기 RNG가 완전히 예측 가능하더라도, 공격자가 모르는 주사위 결과를 충분히 섞었다면 최종 시드는 그만큼 예측하기 어려워진다. 주사위 전용 경로는 기본 RNG를 아예 사용하지 않는다. 단, 이 결론은 펌웨어가 문서에 적힌 계산을 그대로 수행하고 기본 RNG만 잘못된 경우에 적용된다. 악성 펌웨어가 사용자의 입력을 저장·유출하거나 최종 키를 바꾼다면 별도의 검증이 필요하다.
코인카이트는 주사위를 사용한 경우를 예외로 보는 조건도 구체적으로 제시한다. 취약 펌웨어에서 시드를 만들었더라도 공정한 주사위를 매번 따로 50회 이상 굴리고, 그 결과를 외부에 노출하지 않은 채 최종 시드에 추가했다면, 회사는 이 RNG 결함만으로는 해당 시드를 위험하다고 보지 않는다. 코인카이트는 주사위 50–98회면 128비트 이상, 99회 이상이면 약 256비트의 난수를 더한 것으로 본다. 50회보다 적게 굴렸거나 몇 회를 굴렸는지, 결과가 외부에 노출되지 않았는지 기억나지 않는다면 새 지갑으로 자금을 옮기라는 권고를 따라야 한다.
여기서 “주사위가 편향돼도 기기 RNG가 받쳐 준다”는 식으로 말하면 안 된다. 이번 사건에서는 바로 그 기기 입력이 약했다. 이 경우 안전성은 사용자가 실제로 넣은 주사위 결과에 달려 있다. 입력을 잘못했거나 주사위 결과가 촬영·기록돼 외부에 노출됐다면 이 예외는 성립하지 않을 수 있고, 같은 주사위 기록을 여러 지갑에 다시 사용하면 여러 지갑이 같은 원인으로 동시에 무너질 수 있다.
주사위 검증에는 두 가지 방법이 있다
주사위 전용 모드의 계산법은 공개돼 있다. SHA-256(주사위 숫자의 ASCII 문자열)이다. 24단어 모드는 32바이트 해시 전체를 사용하고, 12단어 모드는 그 앞 16바이트를 128비트 엔트로피로 사용한다. 주사위를 한 번도 입력하지 않았을 때 화면에 표시되는 e3b0c442…b855가 빈 문자열의 SHA-256과 일치하고, 123456 같은 공개 테스트 입력도 일반 도구로 재현할 수 있다. 공식 검증 문서가 제공하는 rolls.py·rolls12.py는 해시를 BIP-39 단어로 바꾸는 단계까지 대조한다.
검증 방법은 두 가지다.
[A. 계산 방법만 확인]
폐기할 시험용 주사위 숫자열을 기기와 별도 도구에 넣고 해시·니모닉을 대조한다.
→ 기기가 문서에 적힌 계산을 따르는지는 확인할 수 있지만, 실제 시드를 다시 만든 것은 아니다.
[B. 실제 시드까지 다시 계산]
실제 지갑에 사용할 주사위 숫자열을 검증된 일회용 오프라인 환경에 입력해
해시·니모닉·지갑 지문·수신 주소를 대조한다.
→ 최종 결과까지 직접 확인할 수 있지만, 그 컴퓨터도 함께 신뢰해야 한다.
코인카이트의 두 공식 문서는 이 지점에서 권고 수준이 다르다. Verifying Dice Roll Math는 네트워크와 저장 장치를 분리한 Tails 환경에서 실제 주사위 숫자열을 재현하는 절차를 제시한다. 반면 Paranoid Guide는 실제 지갑에 사용할 주사위 숫자열을 어떤 컴퓨터에도 입력하지 말고, 폐기할 시험용 숫자열로 계산 경로만 확인하라고 권고한다. 이 글의 기본 권고는 더 보수적인 후자다.
일반 데스크톱이나 웹페이지에 실제 지갑용 주사위 숫자열을 입력해서는 안 된다. 네트워크를 끊었다고 기존 운영체제·저장 장치·펌웨어의 신뢰 문제가 자동으로 사라지는 것도 아니다. 실제 시드를 재현하기로 했다면 저장 장치에 흔적을 남기지 않는 일회용 환경을 사용하고, 도구 파일의 출처와 무결성을 따로 확인하며, 네트워크와 영구 저장 장치를 분리하고 작업 뒤 임시 기록을 폐기해야 한다. 이 경우 그 컴퓨터까지 신뢰해야 한다는 점을 받아들여야 한다. 신뢰 대상을 늘리고 싶지 않다면 공개 테스트 벡터로 구현만 시험하고, 실제 주사위 숫자열은 기기에만 입력한다.
주사위 기록만으로도 같은 시드를 다시 만들 수 있으므로, 니모닉과 같은 수준으로 보관하거나 안전하게 파기한다. 촬영 장치·CCTV·음성 비서·클라우드 메모를 피하고, 주소는 지갑 지문과 xpub·디스크립터를 이용한 별도의 조회 전용(watch-only) 환경에서도 다시 확인한다. 커뮤니티 도구 하나의 결과만 믿지 말고, 가능하면 서로 다른 두 프로그램으로 해시와 니모닉, 수신 주소가 같은지 확인한다.
주사위가 막지 못하는 것
사용자가 직접 넣은 난수는 기본 RNG의 우발적인 실패를 피하게 해 줄 수 있지만, 기기는 완성된 시드와 개인키를 보게 된다. 악성 펌웨어는 주소를 바꾸거나, 서명 난수(nonce)에 비밀을 숨기거나, 백업·통신 경로로 유출할 수 있다. 2024년 공개된 Dark Skippy는 악성 서명 장치가 공개 트랜잭션의 서명 난수에 시드 정보를 은닉할 수 있음을 보인 연구용 개념증명이다. 공개 자료상 실제 악용이 확인된 사건은 아니지만, 주사위 결과가 완벽해도 악성 서명기 위협이 남는다는 점을 보여준다.
주사위나 동전이 안전한 시드를 만드는 유일한 방법은 아니다. 그러나 이 글은 니모닉을 처음 만들 때, 사용자가 네트워크에서 분리된 상태에서 공정한 주사위나 동전으로 충분한 난수를 직접 만들고 가능한 범위에서 계산을 독립적으로 확인하는 방식을 가장 바람직한 기본값으로 본다. 기기의 기본 RNG가 아무리 잘 설계됐다고 설명돼도, 사용자는 완성된 니모닉만 보고 그 설명이 실제 구현에서도 지켜졌는지 확인하기 어렵기 때문이다.
다만 주사위나 동전을 사용했다고 모든 문제가 해결되는 것은 아니다. 물리 난수 입력은 기본 RNG를 덜 믿어도 되게 하지만 악성 펌웨어, 주소 바꿔치기, 기록 노출과 잘못된 운용까지 막지는 못한다. 수신 주소를 다른 화면에서 다시 확인하고, 펌웨어와 빌드 과정을 검토하고, 키를 서로 다른 장치와 장소에 나누고, 키 하나만으로 자금을 움직일 수 없게 멀티시그를 구성하는 일은 각각 다른 위험을 줄인다.
7. 하드웨어 지갑은 난수를 어떻게 만드는가 — 제품별 공개 설계 비교
하드웨어 지갑마다 난수를 만드는 방식이 다르다. 제품을 비교할 때는 네 가지를 봐야 한다. (1) 기본 시드를 만들 때 어떤 난수원을 쓰는가. (2) 여러 입력을 어떻게 섞고, 어느 입력이 실패하면 안전하게 멈추는가. (3) 사용자가 주사위처럼 직접 만든 난수를 넣을 수 있는가. (4) 소스 코드, 재현 가능한 빌드, 테스트 벡터와 주소 대조를 통해 실제 생성 과정과 결과를 확인할 수 있는가.
여러 난수원을 함께 쓰는 설계는 최소 하나가 공격자에게 예측 불가능하고, 값을 섞는 방식이 올바르며, 난수원들이 서로 독립적일 때 효과가 있다. 이 조건이 충족되면 난수원 하나가 실패해도 시드 전체가 함께 무너질 가능성을 줄일 수 있다. 입력 개수가 많다는 사실만으로 안전성이 증명되지는 않는다. 반대로 사용자가 시드를 직접 만들었더라도 장치가 입력을 유출하거나 최종 주소를 바꾸면 안전하지 않다. 아래 내용은 2026년 8월 2일 현재 각 제조사가 공개한 설계와 사용 절차를 대조한 것으로, 제품 순위나 독립적인 보안 인증이 아니다.
Coldcard (Coinkite)
콜드카드는 원래 STM32 하드웨어 RNG를 사용하도록 설계됐다. 신형 모델은 여기에 SE에서 가져온 값을 더해 난수 생성기 상태를 한 번 더 초기화한다. 사용자는 기본 시드에 주사위를 추가하거나, 주사위 입력만으로 12·24단어 시드를 만드는 전용 경로를 선택할 수 있다.
이번 사건은 사양표와 실제 호출 경로가 다를 수 있음을 보여줬다. ngu.random이 MicroPython의 Yasmarang 폴백에 연결되면서 제조사의 예비 추정치는 Mk2·Mk3 약 40비트, Mk4·Q·Mk5 약 72비트로 낮아졌다. Block의 2^32 계산은 공격자가 신형 모델의 폴백 상태와 호출 이력을 특정할 수 있을 때, SE 값을 섞은 뒤 나올 수 있는 난수열이 최대 2^32개라는 뜻이다. 코인카이트는 공정한 주사위를 매번 따로 50회 이상 굴리고, 그 결과를 외부에 노출하지 않은 채 최종 시드에 추가했다면 이번 RNG 결함만으로는 위험하지 않다고 본다. 다만 같은 펌웨어의 페이퍼 월렛·복제 키·자동 생성 비밀번호 등 다른 기능까지 자동으로 안전해지는 것은 아니다. 이 사건의 교훈은 “주사위 기능이 있으니 안전하다”가 아니다. 기본 시드 생성 방식과 주사위 방식이 실제로 문서대로 작동하는지 처음부터 끝까지 각각 시험해야 한다.
Trezor (SatoshiLabs)
Trezor의 공개 문서에 따르면 Model One·Model T는 연결된 컴퓨터의 운영체제에서 얻은 엔트로피와 STM32 하드웨어 TRNG를 결합한다. Safe 3·Safe 5는 OPTIGA 보안 칩을 세 번째 입력으로 추가하고, Safe 7은 TROPIC01 보안 칩까지 포함해 네 입력을 사용한다. 한 입력이 약해도 다른 입력이 시드의 예측 불가능성을 지키도록 설계한 것이다.
연결된 컴퓨터의 입력을 사용한다는 사실 자체가 약점은 아니다. 여러 값을 올바르게 섞는다면, 기기가 제공한 값 가운데 하나만 충분히 강하고 비밀이어도 감염된 연결 컴퓨터는 최종 시드를 예측하지 못해야 한다. 다만 각 입력이 실제로 서로 독립적인지, 코드가 문서에 적힌 대로 동작하는지는 따로 확인해야 한다. 일반 초기 설정에는 콜드카드처럼 주사위 결과를 차례로 입력하고 횟수를 세는 화면이 없다. 외부에서 만든 BIP-39 구문을 복원 기능으로 가져올 수는 있지만, 그 경우 외부에서 시드를 만드는 절차까지 함께 신뢰해야 한다. Trezor도 사람이 임의로 고른 단어를 복원하는 방식은 권장하지 않는다.
BitBox02 (Shift Crypto)
BitBox02의 보안 문서는 새 지갑에 다섯 가지 입력을 사용한다고 설명한다. 보안 칩 TRNG, 마이크로컨트롤러 TRNG, 제조 단계에서 기기마다 미리 저장한 난수, 연결 앱의 엔트로피, 기기 비밀번호의 암호학적 해시다. 제조사 설명에 따르면 다섯 입력을 암호학적으로 섞는다. 네 입력에 문제가 생겨도 나머지 하나가 충분히 강하면 공격자가 최종 시드를 예측하지 못하도록 설계했다.
사용자가 직접 난수를 만들어 넣는 방법도 제공한다. 공식 주사위 절차는 주사위와 동전으로 첫 23단어를 정한 뒤 복원 화면에 입력하고, 기기가 제시하는 유효한 24번째 단어 후보 8개 가운데 하나를 다시 무작위로 선택하는 방식이다. 기기가 주사위 횟수를 직접 세는 방식은 아니지만, 기본 RNG를 쓰지 않고 사용자가 만든 BIP-39 시드를 가져오는 공식 절차다. 펌웨어와 앱은 공개돼 있고 재현 가능한 빌드를 지원한다. 다만 재현 가능한 빌드는 배포된 프로그램이 공개 소스와 같은지 확인하는 방법이다. 각 기기의 물리 RNG가 실제로 좋은 난수를 만드는지까지 알려주지는 않는다.
Passport (Foundation Devices)
Passport Core는 공식 설정 문서에서 회로가 공개된 아발란체 잡음 기반 TRNG를 사용하고, 이를 “다른 난수원들과 함께” 12·24단어 시드 생성에 쓴다고 설명한다. 회로 원리와 설계가 공개돼 있어, 폐쇄형 칩 내부 RNG만 사용하는 제품보다 사용자가 확인할 수 있는 부분이 많다. 그러나 이번에 확인한 공식 문서는 기본 생성 때 결합되는 모든 입력과 결합 함수를 하나씩 열거하지 않는다. 따라서 MCU RNG와 보안 칩 RNG를 포함해 정확히 세 입력을 섞는다고 단정하지 않는다.
펌웨어 2.3.0부터는 사용자가 외부에서 만든 첫 11개 또는 23개 단어를 입력하면 기기가 유효한 마지막 단어를 계산할 수 있다. 이 경로를 주사위와 결합하면 기본 생성기를 사용하지 않는 시드를 가져올 수 있다. 회로도와 펌웨어가 공개돼 있어 설계를 검토하기는 쉽다. 다만 조립된 각 기기의 아발란체 잡음 회로가 실제로 충분한 난수를 만들었는지는 별도의 상태 검사와 코드 검토가 필요하다.
Blockstream Jade
Jade의 공개 문서는 사용자 입력, CPU 카운터, 배터리 상태, 주변 온도, 부팅 중 카메라 이미지, 내장 하드웨어 CRNG, 연결 앱(companion app)의 엔트로피를 사용한다고 설명한다. Jade는 Bitcoin Core와 비슷한 32바이트 난수 풀을 유지한다. 이전 상태와 새 입력을 SHA-512로 해시한 뒤, 결과 절반은 다음 상태로 남기고 나머지 절반은 BIP-39 시드를 만드는 데 사용한다.
내장 CRNG의 입력 가운데 하나는 블루투스에 쓰이는 라디오다. No-Radio 펌웨어에서는 CRNG가 이 입력을 사용할 수 없어 엔트로피가 줄어들 수 있으므로, 부팅 때 bootloader_random_enable()을 호출해 라디오 잡음을 한 번 측정하고 난수 풀에 추가한다. 같은 제품이라도 펌웨어 빌드에 따라 엔트로피 경로가 달라질 수 있다는 사례다. Jade는 주사위 등으로 만든 첫 11·23단어를 입력하고 마지막 단어를 계산하는 기능도 공식 지원한다. 배터리 상태·온도·카운터는 그 자체만으로 공격자가 예측하기 어려운 난수라고 보기 어렵다. 따라서 입력 개수보다 최소 하나의 강한 난수원이 실제로 존재하는지, 난수 풀에 값을 섞는 코드가 올바른지가 중요하다.
Keystone
Keystone은 초기화·지갑 생성 화면에서 주사위 결과를 직접 입력하는 기능을 제공한다. 공식 문서는 최소 50회를 요구하고, 256비트 수준을 목표로 할 때 99회를 안내한다. 6장에서 설명했듯 이상적인 주사위 입력만 계산하면 99회는 약 255.91비트이고 100회는 256비트를 넘는다. 다만 SHA-256으로 압축된 최종 출력의 상한은 256비트다. 따라서 제조사가 안내하는 횟수와 주사위 횟수의 수학적 계산을 구분해 읽어야 한다.
이 방법의 장점은 사용자가 주사위 결과와 횟수를 직접 통제할 수 있다는 점이다. 그러나 이번에 확인한 공식 주사위 문서만으로는 기본 시드를 만들 때 어떤 난수원을 쓰고 어떻게 섞는지, 주사위 모드에서도 기본 RNG를 함께 사용하는지까지 알 수 없다. 따라서 “기기 RNG와 주사위를 항상 결합한다”는 표현은 쓰지 않는다. 주사위 경로와 기본 생성 경로를 서로 다른 기능으로 보고 각각 검증해야 한다.
SeedSigner
SeedSigner는 범용 부품으로 조립하는 무저장(stateless) 에어갭 서명기다. 권장 핵심 부품인 Raspberry Pi Zero 1.3에는 Wi-Fi와 블루투스가 없고, 지갑 소프트웨어와는 QR로 통신한다. 공식 기능 목록에는 D6 주사위로 BIP-39 시드를 만드는 경로와 디지털 사진에서 엔트로피를 추출하는 경로가 포함돼 있다.
사용자가 난수를 직접 만들고 키를 영구 저장하지 않으므로, 전용 장치의 기본 RNG와 내부 저장장치를 덜 믿어도 된다. 하지만 사진이 자동으로 고품질 엔트로피가 되는 것은 아니다. 사진 장면이 충분히 복잡한지, 난수를 뽑는 코드가 올바른지, 설치 파일과 부품을 믿을 수 있는지, QR에 표시된 거래 내용과 백업 절차가 맞는지를 사용자가 직접 확인해야 한다. “무저장”도 전원이 켜진 동안 메모리와 화면에 비밀이 존재하지 않는다는 뜻은 아니다.
제품별 비교표
| 제품 | 기본 시드 생성 방식 | 사용자가 직접 난수를 넣는 방법 | 사용자가 확인할 수 있는 부분 |
|---|---|---|---|
| Coldcard | STM32 RNG 사용<br>신형 모델은 SE 값으로 한 번 더 초기화<br>사건 당시 ngu.random이 소프트웨어 PRNG에 연결됨 | Add Dice Rolls<br>12·24단어 주사위 전용 모드 | 소스 코드 공개<br>주사위 계산 문서와 테스트 벡터 제공<br>최종 빌드의 실제 호출 경로는 별도 확인 필요 |
| Trezor | 연결 컴퓨터 + STM32<br>Safe 3·5는 OPTIGA 추가<br>Safe 7은 TROPIC01까지 추가 | 외부에서 만든 BIP-39 복원 가능<br>기기 내 주사위 횟수 계산 기능은 없음 | 하드웨어와 펌웨어 공개<br>입력 구성은 모델별 공식 문서로 확인 |
| BitBox02 | 보안 칩·MCU TRNG<br>제조 단계에서 저장한 기기별 난수<br>연결 앱과 기기 비밀번호 해시 | 주사위로 첫 23단어 생성<br>기기에서 마지막 단어 후보 계산 | 소스 코드 공개<br>재현 가능한 빌드 지원<br>백업과 주소는 기기 화면에서 대조 |
| Passport | 회로가 공개된 아발란체 잡음 사용<br>공식 문서는 다른 난수원도 함께 사용한다고 설명 | 첫 11·23단어 입력 후 마지막 단어 계산 | 회로와 펌웨어 공개<br>개별 기기의 실제 난수 품질은 별도 확인 필요 |
| Jade | 여러 입력을 SHA-512 난수 풀에 섞음<br>No-Radio 펌웨어는 난수 수집 방식이 다름 | 첫 11·23단어 입력 후 마지막 단어 계산 | 사용하는 입력과 난수 풀 구조 공개<br>펌웨어 종류별 차이 확인 필요 |
| Keystone | 기기 기본 RNG로 시드 생성<br>공식 문서만으로 난수원과 결합 방식을 모두 확인하기 어려움 | 기기에서 주사위 결과 입력<br>50회·99회 공식 기준 | 주사위 절차는 공식 문서로 확인 가능<br>기본 RNG 구성은 추가 기술 문서 확인 필요 |
| SeedSigner | D6 주사위 또는 디지털 사진 사용<br>키를 영구 저장하지 않는 DIY 운용 | 사용자가 주사위나 사진으로 난수를 직접 만듦 | 소스 코드 공개와 범용 부품 사용<br>이미지·부품·운영 절차는 사용자가 직접 검증 |
제품마다 대비하는 위험이 다르다. 여러 난수원을 섞는 방식은 기본 생성기의 부품 하나가 실패할 때를 대비한다. 사용자가 주사위를 직접 넣는 방식은 제조사의 기본 난수원에 대한 의존을 줄인다. 소스 코드 공개와 재현 가능한 빌드는 배포된 프로그램을 확인하는 데 도움이 되고, 지갑 지문과 수신 주소를 다른 도구로 다시 대조하면 최종 결과가 맞는지 확인할 수 있다. 어느 한 방법도 나머지를 대신하지는 못한다.
각 제조사의 문서는 설계 의도와 사용 절차를 설명할 뿐, 실제 기기가 언제나 그 약속대로 동작한다는 증거는 아니다. 핵심은 다음 조건이 함께 갖춰졌는지다. 공격자가 예측할 수 없는 난수원이 최소 하나는 있어야 하고, 여러 값을 올바르게 섞어야 하며, 오류가 나면 안전하게 멈춰야 한다. 빌드와 계산 과정을 다시 확인할 수 있어야 하고, 마지막에는 수신 주소도 별도로 대조해야 한다. 장기 보관이라면 한 단계 더 나아가야 한다. 서로 다른 코드와 공급망을 쓰는 장치에서 키를 따로 만들고, 한 제품 계열의 결함만으로는 지출에 필요한 서명 수를 채울 수 없게 구성해야 한다.
8. 같은 RNG를 쓴 기능도 위험은 서로 다르다
Block의 코드 추적에 따르면 문제의 ngu.random은 새 지갑 시드뿐 아니라 페이퍼 월렛 개인키, 임의 Seed XOR 마스크, 일부 복제·USB·Key Teleport·Web2FA 키, Secure Notes의 자동 생성 비밀번호와 HSM 로컬 코드에도 쓰였다. 그러나 같은 RNG를 사용했다고 해서 모든 문제가 마스터 시드 탈취로 이어지는 것은 아니다. 어떤 비밀이 생성됐는지, 공격자가 후보를 확인할 공개키·주소·암호문을 갖고 있는지, 별도의 패스프레이즈나 안전한 키가 남아 있는지에 따라 실제 위험이 달라진다.
취약한 마스터 시드와 함께 노출되는 것들
영향받은 펌웨어의 기본 RNG 경로로 만든 마스터 시드가 복구되면, 그 시드에서 결정론적으로 파생되는 BIP-32 계정·주소·개인키도 함께 노출된다. BIP-85로 만든 하위 니모닉, WIF, XPRV, 바이트 문자열과 비밀번호도 부모 시드가 노출되면 함께 노출된다. 부모를 아는 공격자는 같은 파생 경로를 그대로 재현할 수 있다.
반대로 다른 곳에서 안전하게 만든 시드를 가져온 경우는 다르게 봐야 한다. 다른 안전한 기기나 충분한 사용자 엔트로피로 만든 시드를 콜드카드에 복원해서 사용했다는 사실만으로 그 시드가 이번 생성 결함 때문에 약해지지는 않는다. 사용자가 직접 입력한 비밀번호와 안전한 부모 시드에서 BIP-85로 만든 비밀번호도 이번 ngu.random 결함으로 직접 약해지지는 않는다. 다만 복제·전송·자동 생성 비밀번호처럼 별도의 취약 RNG 기능을 사용했다면 그 기능은 따로 평가해야 한다. 강하고 고유한 BIP-39 패스프레이즈가 붙은 지갑은 공격자가 시드 후보를 찾은 뒤에도 패스프레이즈를 추가로 알아내야 한다.
마스터 시드가 안전해도 따로 위험할 수 있는 기능
아래 기능들은 마스터 시드가 안전하더라도 취약한 펌웨어에서 별도의 ngu.random 출력을 사용했을 수 있다. 공격에 필요한 자료와 실제 피해 범위가 기능마다 다르므로, 한꺼번에 “전부 해독된다”고 표현해서는 안 된다.
- 페이퍼 월렛 — 취약 RNG가 만든 값: 독립된 secp256k1 개인키 / 가능한 영향과 한계: 공격자는 온체인 주소와 대조해 후보 키가 맞는지 확인할 수 있으므로, 해당 페이퍼 월렛 자금이 직접 위험하다. 마스터 시드 기반 지갑까지 자동으로 노출되는 것은 아니다. “Use Dice” 경로는 이 RNG를 우회한다.
- Seed XOR 임의 분할 — 취약 RNG가 만든 값: 무작위 마스크 / 가능한 영향과 한계: 2분할에서 한 조각이 취약 RNG가 만든 마스크
A이고 다른 조각이B = seed XOR A라면, 공격자가B를 확보한 경우 가능한A를 하나씩 시험해 원래 시드를 복구할 수 있다.A만 확보한 경우에는 원래 시드를 확인할 직접적인 기준이 없다. 기본 결정론적 분할은 이 결함과 별개다. - 기기 복제 — 취약 RNG가 만든 값: 일회용 ECDH 개인키 / 가능한 영향과 한계: 복제용 microSD 파일과 공개키를 확보한 공격자가 후보 키를 확인해 전송 백업을 복호화할 가능성이 있다. 따라서 안전한 외부 시드도 취약한 복제 절차를 거쳤다면 별도 검토가 필요하다.
- USB 암호화 — 취약 RNG가 만든 값: 세션용 장치 키쌍 / 가능한 영향과 한계: 캡처한 통신의 기밀성이 약해질 수 있다. 이것만으로 지갑 서명 개인키가 직접 공개된다는 뜻은 아니다.
- 일반 Key Teleport — 취약 RNG가 만든 값: 임시 ECDH 키와 5바이트 보조 비밀번호 / 가능한 영향과 한계: 교환 파일·공개키·암호문을 확보했다면 전송한 시드나 다른 비밀을 복호화할 가능성이 있다. 멀티시그 PSBT 변형은 주로 자식 인덱스만 RNG로 고르므로 안전한 부모 시드의 개인키를 곧바로 드러내지는 않는다.
- Web2FA — 취약 RNG가 만든 값: 장기 TOTP 비밀, 임시 ECDH 키, 요청별 난수(nonce) / 가능한 영향과 한계: 2차 인증 강도나 캡처한 요청의 기밀성이 약해질 수 있다. 실제로 어디까지 악용할 수 있는지는 기능별로 더 분석해야 한다.
- Secure Notes 비밀번호 생성 — 취약 RNG가 만든 값: 12·24단어·고밀도·혼합형 비밀번호 / 가능한 영향과 한계: 가능한 비밀번호 수가 줄어든다. 사용자가 직접 입력한 비밀번호와 안전한 부모 시드에서 BIP-85로 만든 비밀번호는 직접 영향받지 않는다.
- HSM 로컬 코드 — 취약 RNG가 만든 값: 로컬 확인 코드 재료 / 가능한 영향과 한계: 사용자가 확인해야 하는 정책 코드가 예측하기 쉬워질 수 있지만, 이것이 곧 지갑 서명키 노출을 뜻하는 것은 아니다.
이런 공격이 가능하려면 공격자가 주소·공개키·암호문·분할 조각처럼 후보가 맞는지 대조할 자료를 갖고 있어야 한다. 주소처럼 후보를 대조할 자료가 있는 비밀은 훨씬 쉽게 공격할 수 있다. 그런 자료가 있는 비밀값과 없는 비밀값의 공격 난이도를 같게 봐서는 안 된다.
시드가 복구되면 거래 내역도 다시 연결된다
취약한 시드가 실제로 복구되면 공격자는 그 시드에서 주소를 다시 파생해 과거와 현재의 거래를 연결할 수 있다. xpub 자체가 개인키를 공개하지는 않는다. 하지만 xpub이 있으면 공격자는 후보 시드에서 같은 주소가 나오는지 빠르게 대조할 수 있다. 강한 BIP-39 패스프레이즈를 사용했다면 공격자는 그 패스프레이즈까지 알아야 같은 주소를 다시 만들 수 있다.
CoinJoin·PayJoin 같은 공동 거래에서는 나중에 일부 참여자의 소유권이 드러나면, 다른 참여자와 구분되지 않던 범위인 익명성 집합도 줄어들 수 있다. 그러나 “2021년 이후 모든 믹싱이 소급해 무효화됐다”는 식의 보편적 결론은 성립하지 않는다. 영향은 취약 지갑이 실제로 참여했는지, 어떤 입력과 출력이 다시 연결됐는지, 분석자가 어떤 추가 정보를 갖는지에 달려 있다. 새 시드로 만든 지갑으로 자금을 옮기면 미래 자금의 노출을 줄일 수 있지만, 이미 공개 체인에 남은 과거 거래 관계 자체를 삭제할 수는 없다.
피해 범위는 기능 이름만 보고 판단할 수 없다. 취약 RNG가 무엇을 만들었는지, 공격자가 후보를 맞춰 볼 주소·공개키·암호문을 갖고 있는지, 별도의 패스프레이즈나 다른 서명키가 남아 있는지를 차례로 확인해야 한다. 다음으로 필요한 것은 한 기기의 키 하나가 노출돼도 곧바로 지출 조건이 충족되지 않게 하는 구조다.
9. 멀티시그 — 한 기기가 실패해도 자금을 지키려면
공개 온체인 집계만으로는 4,585개 주소 각각의 원래 지갑 구성, 사용한 하드웨어와 백업 방식을 모두 확정할 수 없다. 따라서 공개 자료만으로 “피해 지갑 전부가 단일서명이었다”고 단정할 수 없다. 확실한 원리는 간단하다. 공격자가 복구한 키 하나만으로 지출 조건이 충족되면 공격자는 자금을 이동할 수 있고, 필요한 서명 수를 채우지 못하면 추가 키가 필요하다.
멀티시그 m-of-n은 n개 키 가운데 m개의 서명을 요구한다. 2-of-3 지갑에서 취약한 콜드카드 키가 하나뿐이고 나머지 두 키가 안전하다면, 공격자가 그 한 키를 복구해도 지출에 필요한 서명 두 개를 확보하지 못한다. 반대로 세 키 가운데 둘을 영향받은 펌웨어의 기본 경로로 만들었다면 공격자가 두 키를 복구해 지출에 필요한 두 개의 서명을 확보할 수 있다. Block도 취약한 장치들만으로 구성한 멀티시그는 보호책이 되지 않으며, 공격자가 복구할 수 없는 안전한 키가 서명에 필요한 수만큼 남아 있어야 한다고 명시한다.
제조사와 설계가 서로 다른 장치를 조합하는 이유는 같은 결함이 여러 키에 동시에 영향을 주는 위험을 줄이기 위해서다. 제조사 이름만 다르게 고른다고 충분한 것은 아니다. 가능하면 제조사, 코드베이스, 마이크로컨트롤러, 보안 칩, 시드 생성 절차, 설정에 사용한 컴퓨터와 백업 장소까지 나눠야 한다. 서로 다른 제품도 같은 외부 BIP-39 생성기, 같은 감염된 컴퓨터, 같은 백업 파일이나 같은 공급망에 의존했다면 동시에 무너질 수 있다. 반대로 같은 제조사의 제품이라도 안전한 시드를 각각 따로 만들고 사용 절차까지 분리했다면 위험이 완전히 같지는 않다. 각 키의 실패 가능성을 서로 무관한 것으로 계산하려면, 생성 장치와 절차도 실제로 분리돼 있어야 한다.
2-of-3을 이번 사건에 적용하면 다음처럼 정리된다.
- 취약 키 1개, 안전한 키 2개 — 공격자가 확보한 서명키: 1개 / 공격자의 지출 가능 여부: 불가. 그러나 취약 키를 교체해 구성한 새 지갑으로 이전해야 함
- 취약 키 2개 이상 — 공격자가 확보한 서명키: 2개 이상 / 공격자의 지출 가능 여부: 가능. 지출에 필요한 키 수가 노출됐으므로 긴급 이전 대상
- 세 키 모두 안전하게 생성됐고, 그중 1개를 공격자에게 노출되지 않은 채 분실 — 공격자가 확보한 서명키: 확보한 키 없음 / 공격자의 지출 가능 여부: 남은 두 키로 정상 지출·복구 가능
- 키는 있으나 디스크립터·경로 정보 분실 — 공격자가 확보한 서명키: 해당 없음 / 공격자의 지출 가능 여부: 복구가 복잡해지거나 실패할 수 있음. 지갑 구성 정보도 반드시 백업해야 함
멀티시그는 어떻게 만들고 운영하느냐에 따라 안전성이 달라진다. 각 키는 서로 분리된 환경과 장치에서 만들고 같은 니모닉을 여러 서명기에 복제하지 않는다. 서명기가 지갑 정책 등록을 지원한다면 이를 등록한다. 또한 키 지문과 xpub, 파생 경로, 스크립트 유형이 적힌 디스크립터가 모든 장치에서 같은지 확인한다. 첫 입금 전에는 최소 두 개의 독립된 서명기 화면에서 동일한 수신 주소를 확인하고, 소액 송금과 복구 시험을 거친다. 디스크립터와 xpub은 직접 자금을 움직이는 비밀 정보는 아니지만, 지갑의 전체 주소와 거래 내역을 추적할 수 있으므로 프라이버시에 민감한 정보다. 분실되지 않도록 중복 보관하되 불필요하게 공개하지 않는다.
영향받은 콜드카드 키가 멀티시그에 포함돼 있다면 펌웨어 업데이트만으로 해당 키가 바뀌지는 않는다. 수정된 펌웨어에서 새 시드를 만들고 새 xpub으로 새 멀티시그 지갑을 구성한 뒤, 새 지갑의 수신 주소를 각 서명기에서 검증하고 기존 지갑에서 필요한 서명을 모아 자금을 이전해야 한다. 취약 키 수가 지출에 필요한 서명 수(m)보다 적어 공격자가 당장 자금을 쓸 수 없더라도, 해당 키는 이미 노출된 것으로 보고 교체하는 편이 안전하다.
비트코인 멀티시그는 임시방편이 아니다. BIP-11은 M-of-N 표준 스크립트를 정의했고, BIP-16의 P2SH와 BIP-141의 P2WSH는 복잡한 지출 조건을 더 실용적으로 만들었다. Taproot의 Tapscript에서는 기존 OP_CHECKMULTISIG 대신 BIP-342의 OP_CHECKSIGADD로 다중 키 조건을 표현할 수 있다. 구현 방식은 달라도 여러 독립 키가 지출 권한을 나눈다는 원리는 같다.
비용도 분명하다. 멀티시그는 단일서명보다 설정·복구·상속이 복잡하고, 일반적인 P2WSH 지출은 입력 크기가 더 크고 수수료도 더 높다. 코디네이터 호환성과 디스크립터 백업도 관리해야 한다. 잘못 구성한 멀티시그는 키를 분산해 놓고도 주소 바꿔치기나 복구 실패를 막지 못한다.
그 비용을 감수하더라도 이 글은 상당한 금액을 장기 보관할 때 서로 다른 장치와 절차로 만든 세 키 중 두 개를 요구하는 2-of-3을 권고한다. 핵심은 제조사 수가 아니라 지출 조건이다. 같은 제조사나 같은 장치 계열의 키만으로는 자금을 움직일 수 없게 해야 한다. 또한 지출에 필요한 키가 한 백업 장소에 모두 모이지 않게 해야 한다. 그러면 RNG 호출 경로 오류, 악성 서명 펌웨어, 물리적 탈취나 공급망 결함으로 키 하나가 노출되더라도 곧바로 자금 이동으로 이어지지 않는다.
10. 콜드카드 사용자라면 지금 무엇을 해야 하나
영향 여부는 지금 설치된 펌웨어가 아니라 해당 시드·키·비밀번호를 만들었을 당시의 모델과 펌웨어 계열을 기준으로 확인해야 한다. 같은 니모닉을 새 기기로 복원하거나 펌웨어를 업데이트해도, 처음 시드를 만들 때 사용한 난수의 품질은 바뀌지 않는다. 아래 표는 2026년 8월 2일 기준 코인카이트 공식 권고와 Block의 코드 분석을 함께 정리한 것이다.
- Mk1 — 시드·키를 만든 당시 펌웨어: 출시된 모든 버전, 최대 3.0.6 / 영향 여부: 이번 결함의 영향 범위 밖 / 수정 버전: 해당 없음
- Mk2·Mk3 — 시드·키를 만든 당시 펌웨어: 3.2.2 이하 / 영향 여부: STM32 하드웨어 RNG를 직접 사용하므로 이번 결함의 영향 범위 밖 / 수정 버전: 해당 없음
- Mk2·Mk3 — 시드·키를 만든 당시 펌웨어: 4.0.1–4.1.9(확인된 공개 배포본) / v4.0.0 태그에도 결함 코드가 있으나 서명 배포 목록에는 없음 / 영향 여부: 영향받음, 제조사 예비 추정 약 40비트 / 수정 버전: Mk2·Mk3 4.x: 4.2.0 이상
- Mk4·Mk5 Standard — 시드·키를 만든 당시 펌웨어: 5.6.0 미만 / 영향 여부: 영향받음, 제조사 예비 추정 약 72비트 / 수정 버전: 5.6.0 이상
- Mk4·Mk5 Edge — 시드·키를 만든 당시 펌웨어: 6.6.0X 미만 / 영향 여부: 영향받음 / 수정 버전: 6.6.0X 이상
- Q Standard — 시드·키를 만든 당시 펌웨어: 1.5.0Q 미만 / 영향 여부: 영향받음, 제조사 예비 추정 약 72비트 / 수정 버전: 1.5.0Q 이상
- Q Edge — 시드·키를 만든 당시 펌웨어: 6.6.0QX 미만 / 영향 여부: 영향받음 / 수정 버전: 6.6.0QX 이상
- TAPSIGNER·OPENDIME·SATSCARD — 시드·키를 만든 당시 펌웨어: 모든 버전 / 영향 여부: 코인카이트 발표상 다른 코드베이스로 이번 결함 범위 밖 / 수정 버전: 해당 없음
Standard와 Edge는 서로 다른 펌웨어 계열이다. 숫자만 비교해 “Edge 6.x가 Standard 5.6.0보다 높으니 수정됐다”고 판단하면 안 된다. Mk4·Mk5 Edge는 6.6.0X, Q Edge는 6.6.0QX 이상이어야 한다. Block은 v4.0.0 태그에서 취약 호출 경로를 확인했지만, Wizardsardine은 이 버전이 서명된 배포 목록에 없어 일반 사용자에게 배포되지 않았다고 분석한다. 따라서 일반 사용자에게 배포된 것으로 확인되는 첫 영향 버전은 공식 권고와 같은 4.0.1이다. 직접 빌드하거나 비공식 v4.0.0을 사용했다면 영향 대상으로 본다.
Wizardsardine은 별도로 Mk3용 5.0.1-mk3·5.0.3-mk3도 약 40비트의 영향 버전으로 분류한다. 그러나 2026년 8월 2일 현재 코인카이트의 공식 권고와 공개 다운로드 목록에서는 이 두 표기를 확인할 수 없다. 실제 장치 화면이나 보관 파일에서 이 버전 문자열을 확인했다면, 제조사가 범위를 명확히 할 때까지 영향받은 것으로 보고 새 지갑으로 자금을 옮기는 편이 안전하다.
시드의 안전성은 현재 서명 장치가 아니라 최초 생성 장치와 당시 펌웨어를 기준으로 판단한다. Mk3에서 만든 취약 시드를 Mk4·Q나 다른 제조사의 기기로 옮겨도 같은 시드다. 반대로 Mk2·Mk3 3.2.2 이하의 기존 STM32 RNG 경로 또는 다른 안전한 기기에서 만든 시드를 신형 콜드카드에 복원했다면, 그 시드는 이번 시드 생성 경로 오류만으로 약해지지 않는다. 다만 8장에서 다룬 복제·페이퍼 월렛·자동 생성 비밀번호 같은 별도 기능은 따로 확인해야 한다.
업데이트는 설치 후 새로 만드는 시드와 키의 생성 경로만 바로잡는다. 이미 만든 시드·페이퍼 월렛 키·자동 생성 비밀번호가 다시 강해지는 것은 아니다. 영향받은 시드를 다른 앱이나 하드웨어 지갑으로 가져오는 것도 키 교체가 아니다. 새 시드를 만들기 전에 공식 배포처에서 해당 모델·펌웨어 계열의 수정 펌웨어를 내려받아 설치하고, 기기 화면에서 버전을 확인한다. 수정 펌웨어에서 기본 New Wallet 경로로 새 시드를 만들면 이번 결함을 피할 수 있으며, 주사위 입력은 선택 사항이다.
주사위를 충분히 넣은 경우와 패스프레이즈를 쓴 경우는 다르게 봐야 한다. 공정한 주사위를 매번 따로 50회 이상 굴리고, 그 결과를 외부에 노출하지 않은 채 최종 시드에 추가했다면 코인카이트는 이 RNG 결함만으로는 위험하지 않다고 본다. 50회 미만이거나, 몇 회를 입력했는지·주사위를 추가한 뒤 어떤 시드를 실제로 사용했는지·주사위 결과가 외부에 노출되지 않았는지 가운데 하나라도 확실하지 않다면 새 시드로 만든 지갑으로 자금을 옮긴다. 이 예외는 주사위를 추가한 최종 시드에만 적용되며, 같은 기기에서 별도로 만든 페이퍼 월렛 키나 자동 생성 비밀번호까지 보호하지 않는다.
출처를 모른다는 이유만으로 모든 BIP-39 시드를 콜드카드 취약 시드로 간주할 필요는 없다. 다만 해당 시드가 콜드카드에서 생성됐을 가능성이 있고 모델·생성 펌웨어·주사위 사용 여부를 확인할 수 없다면, 단어만 보고 안전성을 판별할 방법은 없다. 상속받았거나 다른 사람이 설정했거나 기기를 폐기해 기록이 끊긴 경우에는 보유액과 자금을 옮기는 과정의 위험을 함께 고려해, 검증 가능한 새 시드로 만든 지갑으로 자금을 옮기는 편이 안전하다.
패스프레이즈는 추가 방어선이지 취약한 시드를 고치는 수단은 아니다. 강하고 고유한 BIP-39 패스프레이즈가 있으면 공격자는 약한 니모닉 후보뿐 아니라 패스프레이즈도 찾아야 한다. 짧고 흔하며 재사용된 문자열은 사전 공격에 취약하지만, 충분히 길고 무작위인 패스프레이즈를 공격자가 시험하는 데 필요한 계산량을 “거의 0”이라고 표현하는 것도 틀리다. 코인카이트는 강한 패스프레이즈 사용자도 가능한 한 빨리 새 시드로 만든 지갑으로 자금을 옮기도록 권고한다. 여기서 말하는 것은 기기 PIN이 아니라 BIP-39 패스프레이즈다.
이전 순서
- 기존 시드를 지우기 전에 종이·금속 백업, 지갑 지문(XFP), 잔액과 사용 중인 파생 경로를 확인한다.
- 공식 배포처에서 해당 모델·펌웨어 계열의 수정 펌웨어를 내려받고, 가능하면 제조사가 공개한 서명 정보와 파일의 SHA-256 해시를 확인한 뒤 설치한다. 설치 후에는 기기 화면에서 버전을 확인한다.
- 가능하면 수정 펌웨어가 설치된 별도 기기에서 새 시드를 만들어 기존 지갑과 새 지갑을 동시에 유지한다. 한 대만 사용한다면 초기화 전에 기존 백업과 XFP를 반드시 검증하고 코인카이트가 안내한 단일 기기 이전 절차를 따른다.
- 새 니모닉을 정확히 기록하고 복구 시험을 거친 뒤 XFP와 디스크립터(또는 xpub), 수신 주소를 서로 대조한다. 주사위를 사용한다면 공정한 주사위를 매번 따로 굴리고, 그 결과가 외부에 노출되지 않게 해야 한다.
- 기존 지갑에서 검증된 새 주소로 소액을 보내고, 새 지갑에서 XFP·수신 주소·입금을 확인한다.
- 소액 입금과 새 시드의 복구 가능성을 확인한 뒤 기존 지갑의 나머지 잔액을 이전한다. 한 기기를 번갈아 사용하는 경우 새 시드 백업을 다시 검증한 뒤에만 기존 시드를 복원한다. 새 지갑의 전체 잔액과 복구 상태를 확인할 때까지 기존 백업을 폐기하지 않는다.
- 멀티시그라면 취약 키의 xpub만 바꾸는 것이 아니라 새 디스크립터와 새 수신 주소를 가진 지갑을 구성하고 각 서명기에서 주소를 대조한다.
2026년 8월 2일 현재 세 차례의 스윕이 확인됐으므로, 영향받은 단일서명 지갑이나 취약 키만으로 지출에 필요한 서명 수를 채울 수 있는 멀티시그 지갑은 긴급 대응 대상이다. 그러나 서두르다가 백업을 지우거나 비공식 펌웨어를 설치하거나 웹 시드 검사기에 니모닉을 입력하면 오히려 즉각적인 손실이 생길 수 있다. 신속히 대응하되 검증 절차는 생략하지 않는다.
이 글의 입장
이 사건 이후에도 “유명한 제조사가 만들었고 소스 코드가 공개돼 있으니 믿어도 된다”는 결론으로 돌아가서는 안 된다. 뛰어난 개발자도 실수할 수 있고, 명성이 높은 제품도 난수 생성이라는 가장 기본적인 보안 전제를 놓칠 수 있다. 중요한 것은 누구를 믿을지 고르는 일이 아니라, 믿어야 할 범위를 줄이고 한 번의 실패가 모든 자금을 무너뜨리지 못하게 설계하는 일이다.
이 글은 니모닉을 만들 때 네트워크에서 분리된 상태에서 공정한 주사위나 동전으로 충분한 난수를 직접 만들고, 계산 과정과 최종 지갑 지문·수신 주소를 가능한 범위에서 독립적으로 확인하는 방식을 가장 권한다. 상당한 자금은 서로 다른 장치와 절차로 만든 키를 사용하는 2-of-3 멀티시그로 보관해, 기기 하나의 결함이나 개발자의 실수가 지출 권한 전체를 무너뜨리지 않게 해야 한다.
검증할 수 있는 부분은 직접 검증한다. 끝까지 검증하기 어려운 부분은 무조건 믿는 대신 실패의 영향을 제한한다. 이것이 이번 콜드카드 사건이 남긴 가장 중요한 교훈이다.
한 문장 요약
오픈소스와 명성은 안전 보증서가 아니다. 가장 바람직한 출발점은 사용자가 오프라인에서 충분한 물리 난수를 직접 만들고, 검증하기 어려운 기기 실패는 멀티시그로 자금 손실까지 이어지지 않게 하는 것이다.
참고 자료 / 더 읽을거리
아래 자료는 제조사 공식 권고, 독립 코드 분석, 표준 원문, 각 제품의 공식 기술 문서 순으로 정리했습니다. 사건 초기 기사와 집계는 이후 조사로 바뀔 수 있으므로 날짜를 함께 확인해야 합니다.
사건·근본 원인
- Coinkite — COLDCARD Security Advisory: 영향받는 모델·펌웨어 계열, 주사위 예외, 패스프레이즈와 이전 절차
- Coinkite — Entropy Technical Backgrounder: 제조사의 기술적 사후 분석과 수정 내용
- Block Engineering — Predictable RNG Fallback and 32-Bit Reseed in COLDCARD Firmware: 함수 연결, Yasmarang 폴백, 공격자가 아는 정보에 따른 후보 수와 관련 기능 분석
- Wizardsardine — Coldcard RNG Vulnerability: 독립 코드 검토와 기능별 영향 분석
- Bitcoin++ Insider / Dusty Daemon — When random.bytes() runs but doesn't work:
runs·x커밋과 대규모 변경 기록을 통해 문서화와 코드 리뷰의 실패를 비판한 기고문. 개발자의 의도에 관한 부분은 필자의 추정으로 구분해서 읽어야 함 - CoinDesk — 초기 594 BTC 집계
- CoinDesk — 첫 번째 스윕 전체 재구성
- CoinDesk — 세 번째 스윕까지의 8월 2일 집계
표준·과거 사례
- BIP-39: Mnemonic code for generating deterministic keys
- NVD — CVE-2023-31290: Trust Wallet 브라우저 확장
- NVD — CVE-2023-39910: Milk Sad
- Android 보안 난수 경보(2013-08-11) — Bitcoin.org
- Coinspect — Ill Bloom 공식 공개 자료
- Dark Skippy: 악성 서명기가 서명 난수에 시드 정보를 숨겨 유출할 수 있음을 보인 개념증명
콜드카드 사용·검증·멀티시그
- COLDCARD — Master Seed
- COLDCARD — Verifying Dice Roll Math
- COLDCARD — Paranoid Guide
- COLDCARD — Multisig Wallets
- BIP-11 · BIP-16 · BIP-141 · BIP-342
지갑별 공개 설계
- Trezor — 엔트로피 소스와 지갑 생성
- BitBox02 — Security Features · 주사위 시드 절차
- Passport — Passport Core Setup Guide
- Blockstream Jade — 복구 구문 생성 방식 · 마지막 단어 계산
- Keystone — 주사위로 복구 구문 만들기
- SeedSigner — 공식 프로젝트 문서
게시/관리: Corn Academy · Team Corn PTY LTD
본문·도표의 무단 복제·불펌·도용 금지. 공유는 원문 링크 기준으로 부탁드립니다.