2026年10月29日、世界最大級の暗号資産取引所 Binance のホットウォレットから、約63億米ドル相当の資産が流出しました。Binance自身の発表では、侵害は協定世界時(UTC)の20時05分ごろ、日本時間では10月30日の5時05分ごろに始まっています。
この記事は事件の経緯を整理したうえで、多くの解説が触れていない一点を掘り下げます。秘密鍵を安全に保管していても、資産は出ていきうるということです。
鍵を守る仕組み(HSM・MPC・KMS)が守るのは「鍵を盗ませないこと」です。「いま署名しようとしているものが、正当な出金か」を判断するのは、別の層の仕事です。オンチェーンに残った記録は、その別の層が問われた可能性を示しています。その記録と、送金・決済のシステムを作る側が取るべき設計を、テストで動作を確かめたコードで示します。
情報の時点と扱い: 本記事は2026年11月1日1時(日本時間)までに確認した一次資料に基づきます。Binanceは技術的なポストモーテムをまだ公表しておらず、根本原因は確定していません。以下では、公式発表で確認できる事実、調査会社の分析(報道・仮説)、筆者の推論を書き分けます。構成図とコードは一般化した参照モデルで、Binanceの内部構成を復元したものではありません。教材のコードは、実運用の資産管理システムとして使えるものではありません。
30秒で分かる要点
- 何が起きたか: Binanceのホットウォレット(インターネットにつながり、即時の出金に使う財布)が侵害され、約63億米ドル相当が流出した。対象は10前後のチェーンにわたる。
- 原因は確定していない: 初報は「DevOpsエンジニアのノートPCが侵害され、攻撃者がサービスコンテナに入り、不正な出金トランザクションを送った可能性」を挙げた。同日の第2報は、その後の調査で、ほかにも侵入箇所が見つかったと修正した。
- 秘密鍵: Binanceは「現時点で漏えいの証拠は見つかっていない」とする。証拠がないことは、漏えいがなかったことの証明ではない。
- お金の状態は3種類: 約30億ドルが発行体によって凍結された。Binanceは自社資金で流出分の90%を補填した。犯人からの回収額は公表されていない。
- 利用者への影響: 出金は停止中で、11月1日に段階的に再開する予定。入金は通常どおりとされる。
公式発表で確認できる事実と時系列
Binanceは事件について3本の公式発表を出しています。発表ページはJavaScriptで描画されるため、同社のお知らせAPIが返す本文と、公開・更新の時刻(UTC)で照合しました。
| 時刻(UTC) | 日本時間 | 出来事 | 根拠と分類 |
|---|---|---|---|
| 10/28 午前 | 10/28 | 攻撃者の運用ウォレットに Tornado Cash 経由で資金が入る | Cointegrity の分析(報道) |
| 10/29 20:05ごろ | 10/30 05:05ごろ | ホットウォレットの侵害が始まる。最初の流出は XRP Ledger | Binance 第2報(確認済み)。チェーンは Cointegrity |
| 10/29 20:20:59 | 10/30 05:20:59 | ホットウォレット「Binance 14」にEIP-7702の委任が設定される | オンチェーン(確認済み。後述) |
| 10/29 21:01ごろ | 10/30 06:01ごろ | Tether が主な受け取りウォレットを凍結 | Cointegrity(報道) |
| 10/29 22:27〜22:33ごろ | 10/30 07:27〜07:33ごろ | 出金を停止 | Cointegrity(報道)。停止の事実は第1報で確認 |
| 10/29 23:56 | 10/30 08:56 | Binance 14 に入った入金が最後に吸い上げられる | Elliptic(報道) |
| 10/30 02:23 | 10/30 11:23 | 第1報。ノートPCの可能性、鍵の漏えいの証拠なし、約30億ドル凍結 | Binance(確認済み) |
| 10/30 11:36 | 10/30 20:36 | 第1報を更新。「入金は影響を受けておらず通常どおり」と明記 | Binance(確認済み) |
| 10/30 19:33 | 10/31 04:33 | 第2報。20:05の開始時刻、約63億ドル、ノートPC説の修正、80%を補填 | Binance(確認済み) |
| 10/31 14:24 | 10/31 23:24 | 第3報。補填90%、準備金の証明(PoR)のスナップショット、出金は11/1再開予定 | Binance(確認済み) |
初報から第2報で変わったこと
最も重要な変更は、侵入経路の説明です。第1報はこう書いています。
initial investigation suggests that the security incident may involve a compromised DevOps engineer's laptop, which allowed an attacker to gain access to a service container and submit unauthorized withdrawal transactions.
この時点でも「may involve(関係している可能性がある)」という慎重な書き方です。第2報は、この説明自体を修正しました。
Our initial investigation suggested that the incident may involve a compromised laptop, however, our subsequent investigation has uncovered additional points of breach.
「ほかの侵入箇所」が何かは公表されていません。初報だけを引用して「原因はDevOpsのノートPC」と書いた記事は、すでに古い情報に基づいています。
「凍結」「回収」「補填」は別物
報道で混同されやすい3つの言葉を整理します。
| 状態 | 意味 | 金額 | 誰のお金で、どこにあるか |
|---|---|---|---|
| 凍結(Frozen) | 発行体(Tether・Circle等)が、攻撃者のアドレスにあるトークンを動かせなくした | Binance発表で約30億ドル。Ellipticは28.5億ドル(盗難額の44%)、TRMは約27億ドル | 攻撃者のアドレスに残ったまま。戻ってはいない |
| 回収(Recovered) | 盗まれた資産そのものが取り戻された | 公表なし | — |
| 補填(Replenished) | Binanceが自社の資金で、流出した資産と同じ銘柄を用意した | 流出額の90%(第3報)。主要10銘柄は100% | 自社の財務・SAFU(利用者保護基金)・先物保険基金 |
凍結が効いたのは、ほぼUSDTとUSDCだけです。Ellipticは「ビットコイン、XRP Ledger、Solana、NEAR上で盗まれたものは凍結されていない」と書いています。中央の発行体を持つトークンは止められても、ネイティブな資産は止められない、というブロックチェーンの性質が表れています。
秘密鍵についての公式見解
第1報は「We have found no evidence of any wallet private key leak.」、第2報は「At this stage, we have found no evidence that wallet private keys were leaked.」と書いています。第2報では「At this stage(現時点で)」が加わりました。
これは「漏えいしていない」という断定ではありません。後述するオンチェーンの記録は、Binanceの署名システムが、攻撃者の求める署名を生成させられたという見方と矛盾しません。ただし、それだけでは、鍵が持ち出されたかどうかを決められません。
まだ分かっていないこと
- 根本原因と、第2報の言う「ほかの侵入箇所」
- 鍵が持ち出されたのか、署名のサービスが悪用されたのか
- 攻撃者が、後述する50件の事前署名済みの委任をどう入手したか
- 10月30日0時台(UTC)に入金も一時停止したという報道の真偽(Binanceの公式ページは「入金は影響を受けていない」と更新している)
- 補填資金の内訳(SAFU・保険基金・財務の比率)
- 犯行の主体。TRM Labs は北朝鮮の TraderTraitor による犯行と「高い確度で」評価し、Elliptic も「可能性が高い」とする。Binance自身は特定しておらず、政府による帰属の発表も確認できない。これは調査会社の評価であり、確定した事実ではない
基礎知識: 秘密鍵を持つことと、署名させられることは違う
ここから技術の話に入ります。先に、非エンジニアの方にも必要な前提を二つだけ押さえます。
ホットウォレットとコールドウォレット
ホットウォレットは、ネットワークにつながっていて、出金の要求に自動で応じられる財布です。利用者の出金を数秒から数分で処理するために、取引所はある程度の資産をここに置きます。
コールドウォレットは、ネットワークから切り離された環境に鍵を置く財布です。安全な代わりに、資金を動かすには人手と時間がかかります。
取引所の設計は「ホットウォレットにどれだけ置くか」「コールドからどう補充するか」の兼ね合いです。今回の標的はホットウォレットで、それでも被害額が63億ドルに達したことは、取引所の規模の大きさを示しています。
実印と、金庫番のたとえ
ブロックチェーン上の送金は、「秘密鍵による署名」で正当性が決まります。たとえるなら、秘密鍵は会社の実印です。
実印を頑丈な金庫(HSMなど)にしまい、持ち出せなくしても、金庫番に「これに押してください」と書類を渡せる立場を奪われれば、書類には押印されてしまいます。しかも、金庫番が書類の中身を読まずに押す運用なら、書類が「送金依頼」でも「委任状」でも押されます。
このたとえには限界があります。現実の押印は人が内容を読みますが、署名のシステムは、何を見て押すかを、設計した人が決めた規則どおりにしか判断しません。その規則が「金額と送金先だけを見る」だったとき、金額も送金先もない種類の書類が来たら、規則の外にあります。
守る層は三つある: 鍵・署名・業務
「資産を守る」は、少なくとも三つの別の問いに分かれます。
| 層 | 問い | 守る仕組みの例 | 破られると |
|---|---|---|---|
| 1 鍵の保管 | 鍵を持ち出せるか | HSM、MPC、KMS | 鍵が盗まれ、攻撃者が自分で署名できる |
| 2 署名の認可 | 何に署名してよいか | ポリシーエンジン、署名内容のデコード、承認フロー | 正規の署名システムが、望まない署名をする |
| 3 業務の認可 | その出金は業務として正当か | 出金先の許可リスト、限度額、独立承認、異常検知 | 署名は正しい形でも、正当でない資金移動が通る |
多くの議論は、層1に集中します。鍵を守る設計は成熟しており、NIST SP 800-57のような標準もあります。しかし、層1だけでは、層2と層3は守れません。今回のように、層1が(公表の範囲では)破られたかどうか不明なまま、巨額の資金が出ていく事態が起こりえます。
HSM・MPC・KMSは何をして、何をしないか
| 仕組み | 役割 | しないこと |
|---|---|---|
| HSM(専用の耐タンパ装置) | 鍵を装置の外に出さず、装置の中で署名する | 署名の要求の中身が正当かは判断しない。呼び出せる権限を持つ者の要求には応じる |
| MPC(鍵を分割して共同で署名) | 一か所が破られても、鍵全体は盗まれない | 参加者の過半が同じ要求に同意すれば署名する。要求の中身の正しさは別の問題 |
| KMS(クラウドの鍵管理) | 鍵の保管・権限管理・使用の記録 | IAMの権限を持つ主体が署名APIを呼べば署名する |
共通するのは、「誰が署名を依頼できるか」と「何に署名してよいか」を、鍵の仕組みは決めてくれないことです。これは、層2の設計の責任です。
オンチェーンで見えたこと: 金額も送金先もない署名
ここは、公開されている記録で、誰でも確認できる範囲の話です。
確認できた事実
Etherscanに公開されている、2026年10月29日20:20:59(UTC)のトランザクションは、ブロック26,300,753に含まれ、次の特徴を持ちます。
- トランザクション種別は4(EIP-7702 の「コード設定」型)
- 送信元は攻撃者のものとラベルされたアドレス、宛先は「Binance 14」とラベルされたBinanceのホットウォレット
- 50件の認可(authorization)が含まれる。そのうち有効なのは1件(nonce 18914809)で、委任先のアドレスは1つ
筆者は、公開のEthereum RPCから同じトランザクションを取得し、上記の内容(種別4、50件の認可、チェーンID 1、委任先アドレス1つ)を自分で確認しました。
筆者の観測: 同じRPCで「Binance 14」の現在のコードを確認すると、2026年11月1日の時点で空(
0x)でした。これは、現時点では委任が設定されていない(解除されたか、設定が置き換わった)ことを示しますが、いつ・誰が解除したかは確認していません。
EIP-7702とは何か(仕様に書かれていること)
EIP-7702は「Set Code for EOAs」という提案で、ステータスは最終(Final)です。通常、暗号資産のアカウント(EOA)は、鍵で署名して送金するだけで、プログラムを持ちません。EIP-7702は、EOAが、指定したコントラクトのコードを自分のものとして動かせるようにする仕組みです。仕様の要点は次のとおりです。
- 署名の対象は
[chain_id, address, nonce]の3つで、送金額や送金先は含まれません - 認可が有効なら、そのアカウントのコードに
0xef0100 || address(「このアドレスのコードに委ねる」という印)が書かれる - 以後、そのアカウントへの呼び出しは、委ねられた先のコードが、そのアカウントの立場で実行される
- トランザクションの実行が失敗しても、設定された委任は巻き戻されない
仕様の「セキュリティ上の考慮」節は、委任先のコードの設計が悪いと、「悪意のある者が、署名者のアカウントをほぼ完全に制御できる」と明記しています。
何が言えて、何が言えないか
| 言えること(確認済み) | 言えないこと |
|---|---|
| Binance 14 に、EIP-7702の委任が設定された記録がある | 攻撃者がどうやってその署名を入手したか |
| 委任の認可は、そのアカウントの鍵で署名されている必要がある(仕様) | 鍵が持ち出されたか、署名システムが署名させられたか |
| 委任の署名は、金額・送金先を含まない(仕様) | Binanceの署名システムが、この種の署名を検査していたかどうか(内部構成は非公開) |
調査会社のEllipticは、攻撃者が「Binanceの自分のシステムに署名させたように見える」と分析し、Binance側の説明として、この委任が「通常の承認の管理を経なかった」と伝えています。TRM Labsも「ホットウォレットの署名インフラの侵害を指している」と述べます。ただし、これらは分析であり、Binanceの公式な原因の認定ではありません。SlowMistなど、鍵の漏えいの可能性を仮説に残す専門家もいます。
筆者の見解: なぜ「金額と送金先だけ」を見るポリシーは足りないのか
以下は、公表された事実からの筆者の推論です。確定した事実ではありません。
出金のポリシーを「金額が上限以内で、送金先が許可リストにある」と作ると、送金の形をしていない署名(アカウントの動作を変えるもの)は、そもそも判定の対象外になりえます。ポリシーが「送金か、送金でないか」を区別せず、署名する実バイト列を読まない設計だと、この穴が開きます。
今回、Binanceのポリシーがどうだったかは不明です。ただ、設計者が学べるのは、「許可する署名の種類を限定する」という原則です。後述の教材で、これをコードにします。
参照アーキテクチャ: 出金・署名パイプライン
注意: 下の図は、取引所・決済システムの一般的な参照モデルです。Binanceの内部構成を復元したものではありません。
[利用者]──出金要求──▶ [出金API] ──▶ [ポリシーエンジン] ──▶ [署名サービス] ──▶ [ブロックチェーン]
認証・冪等 層2・層3の判定 層1(HSM/MPC/KMS)
キー受付 ・署名する実バイト列 鍵は外に出ない
をデコード
・送金以外は拒否
・限度額を原子的に予約
・承認(独立2名)を検証
│
[DevOps端末] ─ ─ ✕ ─ ─ ▶ 本番の署名経路へ直接つながない(人は、承認の画面だけ)
│
▼
[監査ログ(追記のみ)] ──▶ [異常検知・緊急停止]
脅威モデル: アカウントの侵害から不正出金につながる経路
| 段階 | 起こること | 止める層 |
|---|---|---|
| 1. 足がかり | 開発者の端末・CI・コンテナなどが侵害される | 端末の分離、短命の認証情報、ネットワークの分離 |
| 2. 横展開 | 侵害した主体が、署名サービスを呼べる権限に到達する | IAM・ワークロードのIDの最小権限、署名サービスの呼び出し元の限定 |
| 3. 署名の要求 | 攻撃者が、署名を求める | 層2: 署名する実バイト列の検査、送金以外の既定拒否 |
| 4. 資金の移動 | 署名が通れば、資産が動く | 層3: 出金先の許可、限度額、独立承認、冷却期間 |
| 5. 持ち去り | 資産が出ていき、分散する | 異常検知、緊急停止、発行体との連携(凍結) |
DevOps端末から本番へ: 信頼境界
ただし、第2報で説明は修正され、実際の経路は不明です。第1報が挙げた「DevOps端末→サービスコンテナ→出金」という経路が仮にそのとおりだとすれば、それは人の端末が、本番の資産に近すぎる設計の典型的なリスクにあたります。一般に、次を分けます。
- 開発・運用の端末は、本番の署名経路に直接つながない。必要な操作は、承認フロー(変更管理)を経る
- サービスの権限(ワークロードID)は、そのサービスの役割だけに絞る。コンテナに、出金を指示できる権限を持たせるなら、そのコンテナを乗っ取った者も同じ権限を得る
- 人が持つ長期の認証情報を、本番の署名のAPIに通用させない。短命のトークンと、都度の承認を使う
教材: 署名の前に置く出金ポリシーエンジン
ここから、層2・層3をTypeScriptで実装した教材を示します。コードの全文とテストは、このサイトのリポジトリの tests/unit/fixtures/withdrawal-policy.ts と tests/unit/withdrawal-policy.test.ts にあり、CIで常に実行されます。
限界の明示: これは教育用の参照実装です。実運用の資産管理システムには、永続化されたデータベース、署名サービスとの認証、鍵の運用、監査、障害対応が必要で、この教材はその代わりになりません。
設計の方針
| 要件 | この教材での実装 |
|---|---|
| 通貨と最小単位を明示した金額型 | Money { asset, minor: bigint }。"0.1" のような文字列から、丸めずに整数へ変換。小数が多すぎれば拒否 |
| 浮動小数点誤差の回避 | 金額は常に整数(bigint)。number を使わない |
| 署名する実バイト列の判定 | 呼び出し元の申告ではなく、バイト列をデコードして種類を判断 |
| 送金以外の署名の拒否 | 委任(種別4)・承認(approve)・未知のものは既定で拒否 |
| 不正な出金先の拒否 | 許可リスト+追加後の冷却期間 |
| 承認の束縛・期限・リプレイ対策 | 承認は要求のダイジェストに束縛され、期限を持つ。同じ要求IDの予約は一度きりで、承認済みの要求の使い回しは拒否 |
| 職務の分離 | 要求者本人の承認は無効。異なる2名、異なる2つの役割が必要 |
| 原子的な限度額更新 | 「確認」と「更新」を一つの不可分な操作にする |
| 冪等性 | 同じ冪等キーは同じ判断を返し、二重に予約しない。内容が違えば拒否 |
| フェイルクローズ | 想定外のエラーは拒否に倒す |
| 監査可能性 | すべての判断を、追記のみのログに残す |
| 緊急停止 | サーキットブレーカー(開くと全拒否)。冪等性の保存済みの判断より先に判定するので、許可済みの要求の再送も止まる |
署名する実バイト列をデコードする
export interface SigningPayload {
readonly txType: number; // EIP-2718 のトランザクション種別。0x04 は EIP-7702
readonly to: string;
readonly valueWei: bigint;
readonly data: string; // 16進のcalldata。ETHの単純送金は "0x"
}
export type Intent =
| { readonly op: "native_transfer"; readonly recipient: string; readonly amount: bigint }
| { readonly op: "erc20_transfer"; readonly token: string; readonly recipient: string; readonly amount: bigint }
| { readonly op: "delegation" | "approval" | "unknown" };
const SELECTOR = {
transfer: "0xa9059cbb", // transfer(address,uint256)
approve: "0x095ea7b3", // approve(address,uint256)
setApprovalForAll: "0xa22cb465", // setApprovalForAll(address,bool)
} as const;
/** 呼び出し元のラベルではなく、バイト列を読む。読めないものは "unknown"。 */
export function decodeIntent(p: SigningPayload): Intent {
if (p.txType === 0x04) return { op: "delegation" }; // アカウントのコードを委ねる署名
const data = p.data.toLowerCase();
if (data === "0x") return { op: "native_transfer", recipient: p.to.toLowerCase(), amount: p.valueWei };
if (p.valueWei !== BigInt(0)) return { op: "unknown" };
const selector = data.slice(0, 10);
if (selector === SELECTOR.approve || selector === SELECTOR.setApprovalForAll) return { op: "approval" };
if (selector === SELECTOR.transfer && data.length === 10 + 64 + 64) {
return {
op: "erc20_transfer",
token: p.to.toLowerCase(),
recipient: "0x" + data.slice(34, 74),
amount: BigInt("0x" + data.slice(74, 138)),
};
}
return { op: "unknown" };
}
0xa9059cbb・0x095ea7b3・0xa22cb465 は、それぞれ ERC-20の transfer(address,uint256)・approve(address,uint256)・setApprovalForAll(address,bool) の関数セレクタで、筆者は公開のシグネチャ辞書で確認しました。
ポイントは、許可する種類を積極的に列挙し、それ以外を拒否することです。「危険なものを禁止する」方式(ブラックリスト)だと、知らない種類の署名がすり抜けます。
判定の順序と、「申告」と「実体」の突き合わせ
// 抜粋: WithdrawalPolicy.#decide
if (this.#circuitOpen) return audit(deny("circuit_open"));
// 署名される「実体」を判定する。申告ではない。
const intent = decodeIntent(req.payload);
const expectedToken = this.cfg.tokenContracts[asset]?.toLowerCase();
const kindOk =
(intent.op === "native_transfer" && asset === "ETH") ||
(intent.op === "erc20_transfer" && intent.token === expectedToken);
if (!kindOk) return audit(deny("operation_not_permitted"));
if (intent.recipient !== req.destination.toLowerCase() || intent.amount !== minor) {
return audit(deny("payload_mismatch"));
}
// ...許可リスト・冷却期間・1回あたりの上限・承認の検証...
// 最後に、限度額の「確認と更新」を、一つの不可分な操作で行う:
const reservationId = `rsv_${req.id}`;
const slot = await this.store.tryReserve(reservationId, req.accountId, asset, minor, this.cfg.dailyLimit[asset], now);
if (slot === "duplicate") return audit(deny("request_already_used"));
if (slot === "limit") return audit(deny("daily_limit"));
return audit({ kind: "allow", reservationId });
要求が申告する金額と宛先が、署名される実体と一致するかを確かめるのが payload_mismatch です。申告された内容を信じないのが、層2の基本です。
承認は「特定の要求」に束縛する
export function requestDigest(r: WithdrawalRequest): string {
const p = r.payload;
const canonical = JSON.stringify([
r.id, r.accountId, r.amount.asset, r.amount.minor.toString(), r.destination,
p.txType, p.to.toLowerCase(), p.valueWei.toString(), p.data.toLowerCase(),
]);
return createHash("sha256").update(canonical).digest("hex");
}
承認者が同意するのは「この要求」です。承認をダイジェストに束縛すれば、別の要求への流用(リプレイ)は無効になります。さらに、期限を持たせ、要求者本人の承認を無効にし、異なる2名・異なる2役割を要求します。
競合状態: 「確認してから更新」は破れる
限度額の確認を、次のように書くことがあります。
// 悪い例(RacyStore): 読む → 待つ → 判断する → 書く
const used = this.#used.get(key) ?? BigInt(0); // 1. 読む
await sleep(5); // 2. DBへの往復で待つ
if (used + amount > limit) return "limit"; // 3. 古い値で判断する
this.#used.set(key, used + amount); // 4. 書く
return "reserved";
「読んでから書くまでの間に、別の要求が割り込む」ことをTOCTOU(確認時と使用時のずれ)と呼びます。具体的な数字で見ます。1日の上限が10 ETH、1件が0.9 ETHの要求が同時に50件届くとします。正しくは11件(10 ÷ 0.9 の切り捨て)で止まるはずです。
| 実装 | 50件の同時要求のうち、通った件数 |
|---|---|
| 確認と更新を分けた実装(上の悪い例) | 50件(上限を大幅に超過) |
| 確認と更新を一つの不可分な操作にした実装 | 11件(正しく上限で止まる) |
これは、筆者が実際にテストで測った値です。全件が通ったのは、50件すべてが「まだ誰も使っていない」という古い値で判断したためです。
実際のデータベースでは、UPDATE ... SET used = used + $amount WHERE used + $amount <= $limit のような条件つきの更新(更新された行数で成否を判断)、または行ロックで実現します。教材のメモリ上の実装は、「await を挟まずに読み書きする」ことで、同じ性質を示しています。予約には要求IDごとの一意制約(UNIQUE)も必要で、教材は同じ要求IDの二重予約を request_already_used として拒否します。承認済みの要求を別の冪等キーで再送しても、限度額を二重に使えません。
テスト結果
npx vitest run tests/unit/withdrawal-policy.test.ts の結果は25テストすべて合格(筆者の環境)、tsc の型検査とESLintも通りました。検証した項目は次のとおりです。
- 金額の変換(小数が多すぎる・負・ゼロ・指数表記を拒否)
- 署名のデコード(送金・委任・承認・未知)。ETH付きの呼び出し、途中で切れた・余分なバイトのあるcalldataは読み切れないので拒否。アドレスの大文字小文字は区別しない。USDTの要求にETH送金の署名は通らない
- 許可リストにある宛先への少額の出金は許可(ETHとUSDT)
- 委任・承認・偽のトークンの署名は、要求が無害に見えても拒否
- 申告の宛先・金額と、署名の実体が違う要求を拒否
- 未知の宛先、追加直後(冷却期間中)の宛先を拒否
- 上限超過、承認が必要な額の承認なしを拒否
- 承認の期限切れ、別の要求への流用(宛先・金額・アカウントのどれを変えても無効)、本人承認、同じ役割だけ、同一人の重複、1人が2つの役割を兼ねる承認を拒否
- 承認済みの要求を、別の冪等キーで再送して二重に使うことを拒否(
request_already_used) - 境界値(冷却期間・1回の上限・承認のしきい値・承認の期限がちょうどのとき)と、24時間のローリング窓(上限ちょうどまで使え、24時間後に解放される)
- 冪等性: 同じ要求の再送は一度だけ予約。内容が違えば
idempotency_conflict。一過性の障害は記憶されず、同じキーで再試行できる - 50件の同時要求で上限を超えない(原子的な予約)/確認と更新を分けた実装は超える
- サーキットブレーカーの全拒否(すでに許可された要求の再送も止まる)と、判断のログの記録(変更不可)。監査ストアが落ちても遮断は開く
- ストアの障害が拒否になる(フェイルクローズ)。予約の後で監査ログの書き込みが失敗した場合も拒否になるが、補償処理はないので予約は戻らない(既知の限界として固定)
テストを通すために、不変条件を弱めたり、テストを無効にしたりはしていません。
この教材が示さないこと
- 実際の署名サービスとの接続、鍵の運用、ネットワークの認証
- 永続化された、複数ノードにまたがる限度額の整合性
- EIP-7702の署名そのものの検証(本教材は、署名される前の要求を判定する)
- 異常検知の統計モデル(しきい値の設計)
- 承認者の認証。
approverIdとroleは検証済みの入力を前提にしており、承認の署名そのものの検証は含まない - 日次の累計に対する承認。承認のしきい値は1件ごとなので、小口に分割すれば承認を避けられる。実運用では累計にもしきい値を掛ける
- 補償処理。予約の後で監査ログの書き込みが失敗すると、拒否を返すが予約は戻らない
- 拒否された判断も、同じ冪等キーで保存される。内容を直した要求は、新しい冪等キーで送る
準備金の証明(PoR)で、何が確かめられるか
Binanceは第3報で、10月31日0時(UTC)時点の準備金の証明(Proof of Reserves)のスナップショットを示し、ゼロ知識証明を含む完全な証明を11月3日(UTC)までに公表予定としています。
筆者の理解では、PoRが示すのはある時点で、顧客の預かり資産に見合う資産があるかです。これが示さないことも、同じくらい重要です。
| PoRで確かめられる | PoRでは確かめられない |
|---|---|
| 顧客の残高の合計に対して、保有資産が足りているかの時点の状態 | 鍵や署名の管理が安全か |
| 保有資産の一部が、公開のアドレスにあること | 資産がどう失われたか、再発しないか |
| (ゼロ知識証明を使う場合)顧客ごとの残高を開示せずに、総額の整合を示せること | 負債(顧客への債務)が、すべて含まれているか(設計次第) |
PoRは、「いま足りているか」への答えで、「安全に守られているか」への答えではありません。両者を混同しないことが、取引所を評価するときの基本です。
経営層へ: 鍵を守ってもお金が出ていく理由と、決める順番
非エンジニアの方向けに、この事件から学べる判断を整理します。
3つの問い
- 金庫の鍵の管理は、誰が、どこまで? 鍵の保管(層1)は、専門の製品と手順で守れます。ここは、多くの企業が投資しています。
- 金庫番に「何を押してよいか」の規則は、誰が決めている? 層2です。規則が「金額と送金先だけ」だったり、規則を書いた人が退職していたりしませんか。
- お金が出ていくとき、人の目と、止める手段はあるか? 層3です。高額の出金は二人以上の独立した承認、異常があれば自動で止まる(サーキットブレーカー)、が基本です。
優先順位のつけ方
| 優先度 | 行動 | 目的 |
|---|---|---|
| 高 | 本番の資金移動に使う認証情報・権限の棚卸し。人の端末から本番の署名経路に届くものを除く | 層2の入口を守る |
| 高 | 出金・送金の限度額と、独立承認の閾値を決める。決済・送金の機能を持つなら、同時実行でも上限が守られることを確認 | 層3 |
| 中 | 署名・承認・設定変更の監査ログを、改ざんされない場所に置く | 事後の検証 |
| 中 | 緊急停止の手順を決め、訓練する | 被害の拡大を止める |
| 低 | 公開されたポストモーテムを読み、自社の差分を確認する | 学習 |
決済・送金を扱う事業では、「止まる」ことを機能として設計するのが経営判断です。サービスが止まるのは損失ですが、止められないまま資産が出ていくより小さい、という判断を、平時にしておく必要があります。
チェックリスト
設計・実装
- 署名サービスは、呼び出し元の申告でなく、署名する実バイト列をデコードして判定している
- 許可する署名の種類を列挙し、委任・承認・権限移譲・未知のものを既定で拒否している
- 出金先は許可リストで、追加後に冷却期間がある
- 承認は特定の要求に束縛され、期限を持ち、要求者本人は承認できない
- 限度額の「確認と更新」が原子的で、同時実行のテストがある
- 冪等キーをサーバー側で扱い、同じキーで内容が違えば拒否する
- 想定外のエラーは拒否(フェイルクローズ)になる
- 金額は整数の最小単位で持つ(浮動小数点を使わない)
運用・組織
- 人の端末から、本番の署名経路へ直接届かない
- サービスの権限(ワークロードID)は、役割だけに絞られている
- 判断・承認・設定変更の監査ログが、追記のみで、別の場所に残る
- 緊急停止の手順があり、訓練されている
- 高額の出金に独立した複数の承認が要る(職務の分離)
- オンチェーンとオフチェーンの残高の突き合わせを自動で行っている
現時点の結論と、今後の確認事項
- 事実として言えること: ホットウォレットが侵害され、約63億米ドル相当が流出した。凍結は約30億ドル、補填は90%(Binance自身の資金)、回収は公表なし。出金は停止中で、11月1日に段階的に再開予定。オンチェーンには、EIP-7702の委任がBinance 14に設定された記録がある。
- まだ言えないこと: 根本原因、鍵の漏えいの有無、攻撃者が委任の署名を得た方法、犯行の主体(調査会社の評価は仮説)。
- 技術的な教訓: 秘密鍵の保護(層1)は、署名の認可(層2)と業務の認可(層3)の代わりになりません。何に署名してよいかを、実バイト列で判定し、既定で拒否する設計が、鍵の保護とは別に必要です。
今後確認すべきは、Binanceの技術的なポストモーテム(原因と、第2報の「ほかの侵入箇所」)、Proof of Reservesの完全な証明(11月3日予定)、凍結資産の扱い、出金再開後の状況です。
送金・決済・資産移動の設計をレビューしたい方へ
筆者は、決済プラットフォームの信頼性レイヤー(冪等性・整合性・二重課金の防止)の設計・実装を主導してきました。この記事のような視点で、貴社のバックエンド・決済API・クラウドの権限・資金移動の制御を設計レビューすることができます。
対応できること、できないことを先に書きます。
- できること: 送金・決済のAPI、承認フロー、限度額と冪等性、クラウドのIAMとサービスの権限、監査ログと異常検知の設計レビュー。優先順位つきの改善案と、同時実行・失敗系のテストの設計
- できないこと: 暗号資産のカストディ(保管)の専門的な監査、鍵の管理(HSM・MPCの運用)の評価、インシデントのフォレンジック調査。これらは専門の事業者にご相談ください
- 「完全に安全になる」とはお約束できません。見つかったことと、見ていない範囲を、そのまま報告します
範囲と料金は、下のボタンの先でご確認ください。