COLDCARD のエントロピー障害:サイレントな RNG フォールバックはいかにしてユーザーに 3,800 万ドルの損失をもたらしたか

キーストーン
- COLDCARD Mk3 でシードフレーズを生成・使用したユーザー:直ちに別のウォレットでシードフレーズを再生成し、資金を新しいウォレットへ移行してください。
- COLDCARD Mk4 または Mk5 でシードフレーズを生成・使用したユーザー:元のシードフレーズを生成したファームウェアが 5.6.0 未満だった場合、直ちに別のウォレットでシードフレーズを再生成し、資金を移行してください。
- COLDCARD Q でシードフレーズを生成・使用したユーザー:元のシードフレーズを生成したファームウェアが 1.5.0Q 未満だった場合、直ちに別のウォレットでシードフレーズを再生成し、資金を移行してください。
- OneKey ハードウェアウォレットのユーザーはこの不具合の影響を受けず、安心して使い続けられます。
暗号技術において「ランダム」は厳格な指標だ。それは定量化できるものであり、現実の計算能力に耐えなければならないものである。
12 語のリカバリーフレーズは 128 ビットの乱数に裏打ちされ、BIP-39 仕様に従って人間が読める単語へエンコードされる。この数値がどれだけ予測不可能かが、ウォレットが総当たり攻撃に耐えられるかどうかを直接左右する。128 ビットとは 2¹²⁸ 通りの可能性を意味する。地球上のすべての計算能力を結集しても、宇宙が終わるまでに列挙し尽くすことはできない。
だから理論上、ハードウェアウォレットはこの点に厳格であるべきだ。回路ノイズという物理世界のランダム性を取り込む専用の真性乱数生成器(TRNG)があり、EAL 認証を受けたセキュアエレメント(SE)と組み合わされ、最後に成熟したアルゴリズムがそれらすべてを撹拌する――そうあるべきなのだ。
2026 年 7 月 30 日、500 台近くの COLDCARD ウォレットから約 594 BTC(約 3,800 万ドル)が 30 分足らずで抜き取られた。翌日、製造元の Coinkite は「Technical Deep Dive into the Entropy Issue」[1] で根本原因を認めた。2021 年 3 月のファームウェア変更以降、ビルド構成のミスによって、コードが MicroPython 内蔵のソフトウェア擬似乱数生成器(PRNG)へ静かにフォールバックし続けていたのだ。
影響はモデルによって異なる。Coinkite の暫定的な見積もりでは、設計目標である 128 ビットに対して、Mk3 では有効な探索空間が約 40 ビットしか残っておらず、Mk4、Mk5、Q では約 72 ビットだという。40 ビットはおよそ 1.1 兆通りで、依然として天文学的に聞こえるが、GPU クラスタをレンタルできる余裕のある攻撃者にとっては、「物理的に不可能」から「期待値として実現可能なエンジニアリング課題」へと転落したことを意味する。128 から 40 へ、探索空間は 2⁸⁸ 分の 1、すなわち 30 万×10²⁰ 分の 1 にまで縮小した。
この 3,800 万ドルの窃盗は、その期待を既成事実に変えてしまった。
注:OneKey ユーザーの資金は安全です。OneKey のコードはこの問題に一切関与しておらず、関係する実装や依存関係も使用していません。全文はこちら:https://x.com/OneKeyHQ/status/2083106588022509974?s=20
TL;DR
- COLDCARD Mk3 でシードフレーズを生成・使用したユーザー:直ちに別のウォレットでシードフレーズを再生成し、資金を新しいウォレットへ移行してください。
- COLDCARD Mk4 または Mk5 でシードフレーズを生成・使用したユーザー:元のシードフレーズを生成したファームウェアが 5.6.0 未満だった場合、直ちに別のウォレットでシードフレーズを再生成し、資金を新しいウォレットへ移行してください。
- COLDCARD Q でシードフレーズを生成・使用したユーザー:元のシードフレーズを生成したファームウェアが 1.5.0Q 未満だった場合、直ちに別のウォレットでシードフレーズを再生成し、資金を新しいウォレットへ移行してください。
- OneKey ハードウェアウォレットのユーザーはこの不具合の影響を受けず、安心して使い続けられます。
ここからが本格的な技術解説だ 👇
根本原因:静かに起きたソフトウェアフォールバック
ハードウェア RNG インターフェースから libNgU へ
2021 年の暗号スタック移行により、ウォレットのシード生成は ckcc.rng_bytes() から ngu.random.bytes() へ変更された [6]:
ngu.random.bytes() → libNgU rng_get() → 最終的にリンクされる rng_get シンボル
リスクは関数名や出力長にあるのではない。その最終的な rng_get() を実際にどのオブジェクトファイルが供給するか、にある。
#ifndef はマクロが定義されているかだけを見て、その値は見ない
対象ボードの構成では MICROPY_HW_ENABLE_RNG が 0 として定義されており、MicroPython のハードウェア RNG パスが有効化されていないことを意味していた。
しかし当時、libNgU は次のように書かれていた [8]:
#ifndef MICROPY_HW_ENABLE_RNG
#error ...
#endif
#ifndef はマクロが存在するかどうかだけをテストする。0 として定義されたマクロも依然「定義済み」であるため、不正なビルドを阻止するはずだった #error は一度も発動しなかった。
同名シンボルによって、誤った実装が最終ファームウェアに紛れ込んだ
ハードウェア RNG が有効化されていない場合でも、MicroPython は同じ名前で rng_get() を提供する。ただしそれは Yasmarang ソフトウェア PRNG のフォールバックである [9]。
こうして障害の連鎖が完成した:
マクロは存在するが値が 0 → libNgU のビルドガードが発動しない → ファームウェアは問題なくコンパイルされる → 同名の rng_get が MicroPython のフォールバックに解決される → シード生成が本来受け取るべきハードウェア RNG 入力を受け取れない
一つひとつを取れば、この連鎖のどのリンクも些細な見落としにすぎない。しかし連なると、ビルドシステムのゲートキーピングを無意味にしてしまう。安全な実装がソースやバイナリに存在することは、セキュリティ上重要な呼び出しが実際にそこへ到達することを意味しないのだ。
エントロピー源を混ぜ合わせても、欠けたエントロピーは補えない
フォールバックの初期状態には UID の一部、SysTick、RTC 時刻、サブ秒単位の状態が含まれ、その後 Yasmarang が決定論的にそれを進める。libNgU はさらに、固定の初期状態から始まる第 2 の Yasmarang ストリームとそれを混合する。
再構築または列挙可能な 2 つの決定論的ストリームは、XOR や SHA-256 を通しても新たな物理的予測不可能性を得られない。ハッシュ化は入力を整えることはできても、有限な候補集合を真の 2^256 の未知空間へ拡張することはできない。
40 ビットと 72 ビットが実際に意味するもの
Coinkite が用いる有効探索空間の見積もりは、測定された、あるいは認証されたエントロピー値ではない。この 2 つの数値は、攻撃者が処理しなければならない候補状態空間の大きさを表している。min-entropy の測定値ではなく、平均的な推測回数に変換されるものでもなく、ましてや特定の攻撃時間やコストを導き出すものでもない。
実際の攻撃コストは前提条件の連鎖に依存する。攻撃者がデバイス UID を知っている、あるいは推測できるか;RTC、SysTick、起動時刻の範囲を絞り込めるか;シード生成前に何回の RNG 呼び出しが発生したか;候補をオフラインで検証するための xpub、アドレス、公開鍵を手元に持っているか;そして BIP-39/BIP-32 の導出と並列ハードウェアの実行コストがどれほどか、である。
だから正確な言い方はこうだ。Coinkite の現在の暫定的な攻撃前提のもとでは、Mk3 の有効探索空間は約 40 ビット、Mk4/Q/Mk5 では約 72 ビットと見積もられる。これは「すべての Mk3 ウォレットが正確に 40 ビットのエントロピーを持つ」ことと同じではなく、「新しいモデルは 72 ビットの暗号学的安全性を持つと証明された」ことでもなく、まして「どんな普通のコンピュータでも一定時間内にすべてのウォレットを復元できる」ことでもない。
なぜ Mk4、Mk5、Q も影響を受けるのか
Mk5 と Q のボード上にある 2 つのセキュアエレメント(出典:coldcard.com)
Mk5 と Q のボード上にある 2 つのセキュアエレメント(出典:coldcard.com)
Mk4、Mk5、Q は確かに 2 つのセキュアエレメントからランダムな材料を取り込むが、修正前の実装ではその出力すべてを暗号学的 DRBG に投入していなかった。
公開ソースを見ると、セキュアエレメントの材料をハッシュ化した後、わずか 4 バイトだけを取り出して ngu.random.reseed() へ渡し、Yasmarang の 32 ビット状態ワードをたった 1 つ置き換えているだけだ。
したがってフォールバック状態の残りと呼び出しトレースが固定されている場合:
セキュアエレメントの再シードによって区別可能な出力ストリーム数 ≤ 2³²
これは公式の「約 72 ビット」と矛盾しない。72 ビットモデルは UID、タイマー、RTC、呼び出し履歴といった状態の不確実性も算入している。Block Engineering の緩やかな結合上限は 2^73.3 を下回っており、彼らはこれが 73 ビットの暗号学的安全性を認証するものではないと明言している [10]。
言い換えれば、Mk3 は最悪のケースだ。安全な再シードがまったく存在しないからである。Mk4/Q/Mk5 のセキュアエレメント入力はリスクを下げるが、4 バイトの再シードでは設計が目指していた 128 ビットの安全性を回復できない。
公式勧告と実際の範囲
Coinkite の現在の勧告は Mk3 4.0.1 から始まっている。しかし変更不可能な v4.0.0 ソースにはすでに該当するエントロピー生成パスが含まれており [7]、4.0.0 と 4.0.1 の間に対応する RNG 修正は存在しない。
Block Engineering はさらに踏み込み、Mk2/Mk3 v4.0.0〜v4.1.9 もソース解析に含めている [10]。
つまり、区別すべき 2 つのレベルがある:
- Coinkite の公式範囲:Mk3 4.0.1 以降;
- ソースコードが裏付ける技術的範囲:Mk2/Mk3 v4.0.0〜v4.1.9。
Coinkite が Mk2 や v4.0.0 を公式に確認したとは言えない。同時に、勧告が名指ししていないというだけでそれらが安全だと推論することもできない。
修正はどのように機能するか
Mk4/Mk5 5.6.0 および Q 1.5.0Q のホットフィックスは、1 つの条件式を変更する以上のことを行った。ビルドレベルの強制を追加したのだ [3][4][5]:
- MicroPython のフォールバック RNG オブジェクトを除外する;
- ボードレベルのオブジェクトがグローバルな
rng_get()を提供することを確認する; nmを使ってオブジェクトファイル内のシンボルを検査する;- フォールバックが依然としてそのシンボルをエクスポートしている場合、またはボードレベルのオブジェクトが
rng_get()を正しく提供していない場合、ビルドを直ちに失敗させる。
ユーザーが取るべき対応
Mk4、Mk5、Q
- まず 5.6.0 以降(Mk4/Mk5)、または 1.5.0Q 以降(Q)へ更新する。
- 修正済みファームウェア上で真新しいシードを生成する。
- バックアップを記録し、リストア検証を完了する。
- デバイス画面でウォレットのフィンガープリントと受取アドレスを確認する。
- 残りの資金を移行する前に、少額のテスト送金を行う。
- すべての送金が確認されるまで古いバックアップは保持するが、古いウォレットでの受取は停止する。
Mk3
現時点で Mk3 向けの修正ファームウェアはリリースされておらず、4.1.9 は修正版ではない。後から出るかもしれないファームウェアを待つべきではない。なぜなら、新しいファームウェアであっても既存のシードにエントロピーを追加することはできないからだ。
Coinkite の現在の移行ガイダンスに従うこと。信頼できる経路で新しいシードを生成し、バックアップ、アドレス、少額取引を検証したうえで、資産を移行する。
2 つの異なるサイコロの閾値
サイコロには 2 つの数字がある:50 と 99。両者は異なる目的に用いられ、互換的には使えない。
元のシード生成時に追加した 50 回以上のロール。 シードを最初に生成した際に、記録も保存も開示もされていない、公正で独立した私的な六面ダイスのロールが少なくとも 50 回混入されていた場合、Coinkite はその RNG 問題単独ではそのシードがリスクにさらされているとは見なさないとしている [11]。
50 × log₂(6) ≈ 129.25 ビット
これはこの特定のインシデントに対するおよそ 128 ビットの閾値であって、あなたの運用プロセス全体に対する絶対的な安全証明ではない。
空の Mk3 での Dice Roll Import フロー。 Coinkite は別のフローも提供している。4.1.9 を実行している空の Mk3 で Import Existing > Dice Rolls を選び、少なくとも 99 回の公正なロールを入力する。この専用経路はダイスの並びを直接消費し、影響を受けたデバイスの生成器を使用しない。
99 × log₂(6) ≈ 255.91 ビット
99 は公式フローにおける閾値であり、文字どおり「少なくとも 256 ビット」という基準から逆算すると 100 回が必要になる。
パスフレーズは別個の障壁であって、修正ではない
強力で一意かつランダムな秘密の BIP-39 パスフレーズは異なるウォレットを導出し、独立した探索要素を追加できる。デバイスの PIN はローカルアクセスを制御するだけで、BIP-39 のルート鍵導出には一切関与しない [12]。
しかしパスフレーズは、ニーモニックがそもそも持っていなかったエントロピーを補うことはできない。短いパスフレーズ、有名な引用句、予測可能なパターン、使い回しのパスワードは依然として推測されうる。入力ミスもまた、一見有効に見えるがまったく別のウォレットを生成してしまうため、フィンガープリントを検証し、リストアをテスト済みの信頼できるバックアップを保持しなければならない。
おわりに
このインシデントが露呈させた核心的な問題は、セキュリティ上重要な実装が実際に到達可能かどうかが、ビルドシステムによって一度も強制されていなかったことだ。「誰かがマクロを間違えた」では到底説明しきれない。
少なくとも、ハードウェアウォレットやその他の鍵生成システムは以下を行うべきだ:
- セキュリティ API が最終的にどのオブジェクトとシンボルにリンクするかを検証し、関数シグネチャを確認するだけで済ませない;
- 能力チェックにマクロの存在と値の両方を検証させる;
- セキュリティ上重要なフォールバックをフェイルクローズにし、通常の PRNG を静かに供給することが決してないようにする;
- CI にオブジェクト構成、リンクマップ、シンボルの由来、エンドツーエンドのデータフローを検査させる;
- 出力長や非ゼロ性、重複の有無を確認するだけでなく、本物のハードウェアエントロピーが最終的なシードに到達することをテストで証明する;
- シードを生成したバージョン、現在のファームウェア、修正版、そして古いシードをどうすべきかを明確に区別した勧告を書く。
COLDCARD がオープンソースであったからこそ、外部の研究者はこの呼び出し連鎖を再構築でき、Coinkite は正式な技術レポートと修正リリースの両方を公開できた。
あるブランドを「絶対に安全」と主張することには何の価値もない。価値があるのは、「ランダム性がどこから来て、最終的に何にリンクし、障害時にどこへ向かい、それを出荷製品の中でどう検証するか」を、何度でも再確認できるシステム上の性質にすることだ。
参考文献
[1] Coinkite: Technical Deep Dive into the Entropy Issue [2] Coinkite: Mk3 Security Advisory [3] Coldcard 5.6.0 / 1.5.0Q fix commit [4] Coldcard 5.6.0 release commit [5] Coldcard 1.5.0Q release commit [6] 2021 libNgU seed generation migration commit [7] Official v4.0.0 source snapshot [8] libNgU random.c [9] MicroPython commit introducing the fallback [10] Block Engineering technical analysis [11] Coldcard dice roll math [12] Coldcard passphrase documentation [13] OneKey trezorcrypto.random source branch [14] OneKey seed generation logic [15] OneKey standard Makefile [16] OneKey SCons config






