保住私鑰之後,誰有權要求系統簽章:從 2019 年幣安事件看託管簽署的授權邊界
假設提款服務已被入侵,驗收時就讓它試著取得一張不該核准的交易簽章。本文要測這道授權邊界:即使金鑰留在原處,只認呼叫者身分的簽署端,仍可能照攻擊者的要求簽章。
2026 年幣安事件在這裡只作待驗證的引子:TRM 的評估是,最可能的解釋是攻擊者已能讓幣安的熱錢包簽署系統,跨鏈簽下其選擇的交易。報告所述的 EIP-7702 委派,是讓錢包簽出授權,交由指定程式代為執行;其他鏈則有一般簽署轉帳。
2019 年公告說了什麼
幣安在 2019 年公告中列出的失竊資料,是使用者 API key、2FA 驗證碼及「可能的其他資訊」。公告稱,一筆交易提走 7,000 BTC,交易執行後才觸發警報,接著暫停提款。它也稱這筆交易的結構通過了既有安全檢查。公告說會用 #SAFU fund 全額覆蓋這次事件,使用者資金不受影響。
這份公告的存檔全文沒有出現「private key」。我的解讀是,不能拿它證明熱錢包私鑰遭竊,也不能反過來證明私鑰安全。後文只假設上游失陷,金鑰仍在簽署端,不替事件補完攻擊鏈。
五個職責,分開驗收
建議用以下分工設計系統,別讓「前一關放行」成為後一關唯一的理由。
| 職責 | 在本文模型中負責什麼 |
|---|---|
| 帳號驗證 | 確認登入者身分,交付可驗證的身分證明。 |
| API 權限 | 決定該身分可呼叫哪些端點,不能代替單筆提款核准。 |
| 提款核准 | 核准者同意這筆具體交易,產出綁定內容的核准證明。 |
| 風控 | 判斷交易是否異常,決定拒絕或轉人工檢查。 |
| 密碼學簽署 | 對指定內容產生簽章,前置條件須由簽署服務另行驗證。 |
把核准帶到簽署端
假設金鑰放在 HSM,也就是保管金鑰、執行密碼運算的硬體安全模組。PKCS #11 規範在 CKA_SENSITIVE 為真或 CKA_EXTRACTABLE 為假時,禁止部分私鑰屬性以明文在 token 外揭露。
KMS 在這個模型裡指代管金鑰的服務,程式透過 API 請它簽章。AWS KMS 的 DIGEST 模式要求輸入長度符合指定演算法的雜湊長度。我的建議是,別把這個長度檢查算成提款核准。
MPC 在這裡指多方各持一份金鑰材料,合作完成簽章。NIST 說門檻密碼運算不必先重組金鑰,也說足夠份額可以重建金鑰,不能據此宣稱所有方案都永遠不會重建。
下圖是本文假設,與任何公司的實際架構無關。正常流程經過政策檢查及獨立核准,再送簽署與廣播;紅色虛線假設提款服務失陷,直接呼叫只驗身分的簽署端。無論選哪種金鑰方案,都先假設它不替你決定這筆提款該不該放行。

建議讓簽署端自己驗核准證明,並把核准綁定交易內容。若上游只要送一句「已核准」就過關,這個假設中的攻擊者也送得出來。
核准者的管理權限也應分開,提款服務不能自行新增核准者,或改掉簽署端認得的核准金鑰。否則在這個模型裡,失陷服務連誰算核准者都能決定,這道檢查也就失去獨立性。
範例:改了金額,核准就不適用
以下用 HMAC 模擬簽章,金鑰字串只是範例材料,不描述 AWS KMS 的行為。先把白名單與單筆上限放在簽署端,再要求風控與營運各自核准同一筆內容。
import hashlib, hmac, json
from dataclasses import dataclass
# 假設性的託管模型。HMAC 金鑰當成 HSM 裡不可匯出的簽章金鑰的替身,
# 重點在「誰能叫它簽」,所以不用真的 ECDSA。
HSM_KEY = b"never-leaves-the-box"
APPROVER_KEYS = {"risk": b"risk-approver-key", "ops": b"ops-approver-key"}
def tx_digest(tx: dict) -> str:
return hashlib.sha256(json.dumps(tx, sort_keys=True).encode()).hexdigest()
def approve(who: str, tx: dict) -> str:
return hmac.new(APPROVER_KEYS[who], tx_digest(tx).encode(), "sha256").hexdigest()
@dataclass(frozen=True, slots=True)
class Policy:
allowlist: frozenset[str]
per_tx_limit: int
required: frozenset[str]
POLICY = Policy(frozenset({"addr-user-1", "addr-cold-1"}), 5_000, frozenset({"risk", "ops"}))
def naive_signer(tx: dict) -> str:
# 呼叫者有權限就簽,不檢查交易內容
return hmac.new(HSM_KEY, tx_digest(tx).encode(), "sha256").hexdigest()
def policy_signer(tx: dict, approvals: dict[str, str]) -> str:
if tx["to"] not in POLICY.allowlist:
raise PermissionError("收款地址不在白名單")
if tx["amount"] > POLICY.per_tx_limit:
raise PermissionError("超過單筆上限")
for who in sorted(POLICY.required):
want = approve(who, tx) # 真實系統是驗核准者的公鑰簽章,這裡簡化
if not hmac.compare_digest(approvals.get(who, ""), want):
raise PermissionError(f"缺少 {who} 對這筆交易的核准")
return naive_signer(tx)
legit = {"to": "addr-user-1", "amount": 1_200, "chain": "eth", "nonce": 7}
evil = {"to": "addr-attacker", "amount": 1_200, "chain": "eth", "nonce": 7}
ok = {w: approve(w, legit) for w in POLICY.required}
print("naive evil :", naive_signer(evil)[:16], "(簽出來了)")
print("policy legit:", policy_signer(legit, ok)[:16])
cases = [
("提款服務失陷,直接送 evil", evil, {}),
("拿 legit 的核准去配 evil", evil, ok),
("白名單地址但改金額", {**legit, "amount": 4_999}, ok),
("超過上限", {**legit, "amount": 9_000}, ok),
]
for name, tx, ap in cases:
try:
policy_signer(tx, ap)
print(f"{name}: 簽出來了")
except PermissionError as e:
print(f"{name}: 拒絕,{e}")
naive evil : f1a2c0547991bea7 (簽出來了)
policy legit: 629d32ce5d44f73a
提款服務失陷,直接送 evil: 拒絕,收款地址不在白名單
拿 legit 的核准去配 evil: 拒絕,收款地址不在白名單
白名單地址但改金額: 拒絕,缺少 ops 對這筆交易的核准
超過上限: 拒絕,超過單筆上限
這個模型的前兩個惡意請求都先被白名單擋下,尚未測到核准檢查。把合法交易金額由 1,200 改成 4,999,才走到核准檢查並失敗;9,000 則先超過上限。請用這些差異判讀輸出,別把每次拒絕都算成同一道防線的功勞。
實作時應讓核准者持有各自的私鑰,由簽署端驗公鑰簽章,避免照抄共享 HMAC 金鑰。也應加入核准有效期與防重放紀錄,並綁定政策版本;範例中的 nonce 只列入雜湊,請勿當成已完成防重放。
驗收要拿失陷服務來測
建議用失陷提款服務的憑證跑驗收,檢查它能否讓下列請求取得簽章:
- 跳過政策與核准,直接要求簽章。
- 挪用舊核准,或核准後改收款地址、金額。
- 用同一位核准者冒充第二位。
- 超過限額,或提到仍在冷卻期的新地址。
- 憑證已撤銷,或緊急停用後繼續送請求。
這些測試應全部拒絕,原因要留在簽署端紀錄,每次修改政策都放進 CI 重跑。另測同筆請求重送與過期核准,不要只測內容被改過的情況。限額與停用可接著看〈沒設定就當成零:預設硬上限、four-eyes 門檻與 kill switch 故障方向〉。
成本要一起寫進需求
建議把等待時間與營運負擔一起估進去。地址冷卻期要讓使用者等;累計限額要維護計數,大額提款要安排人工處理。獨立核准應分開保管權限,若由人操作,就要排夜間輪值。撤銷應有獨立路徑,緊急停用則要訂好解除權限,並承擔誤停時合法提款也停擺的代價。
停用多久生效,請實測。AWS 的 DisableKey 文件說停用會阻止金鑰用於密碼運算,也註明 API 採最終一致性,沒有提供立即生效或固定分鐘數的保證。建議持續探測簽署結果,把停用延遲列入驗收。
尚未釐清的事
本文引用的事件來源查核至 2026 年 11 月 1 日。TRM 當時稱幣安尚未公布根因,幣安 2026 年 10 月 30 日的公告則稱當時未發現錢包私鑰外洩的證據。這些說法不能代表本文發文日的調查進度,2019 年公告所稱的「其他資訊」、個別帳號權限與內部簽署架構,也不在本文可確認的範圍內。
補充筆記
- 2019 年公告列出 API key 與 2FA 碼,該公告存檔沒有提到私鑰。
- 建議簽署端自行驗核准,並讓核准綁定完整交易內容。
- 驗收從服務已失陷開始,補測重放與停用生效時間。
- 白名單等待時間與人工核准輪值,應一起列入需求。
延伸閱讀
- Binance Security Breach Update:幣安的當事方公告,本文引用 2019-05-12 10:08:08 UTC 的 Wayback 快照,只取公告裡的說法,不將它視為完整鑑識結論。
- TRM Labs 的 2026 年幣安事件分析(2026 年 11 月 1 日讀取):調查方的分析,本文保留其評估語氣,簽署系統失陷仍待核實。
- Security Incident Update #2:幣安的當事方聲明,本文引用 2026-10-30 22:01:35 UTC 的 Wayback 快照,「未發現」只代表該快照中當時的調查說法。
- PKCS #11 v3.2:OASIS 規範,可核對私鑰屬性的限制,本文不據此替特定設備背書。
- AWS KMS Sign:AWS 官方 API 文件,可核對雜湊處理與長度條件,本文不將它延伸成交易核准保證。
- NIST IR 8214:NIST 的門檻密碼學文件,適合核對一般定義,本文不將其套成所有 MPC 產品的實作保證。
- AWS KMS DisableKey:AWS 官方 API 文件,可核對停用語意,但沒有提供固定生效時間。
想法與技術判斷出自 Sheng,和 Claude 一起起草 · 題目由 Sheng 選定,內文、案例整理與範例由 Claude 撰寫,範例在 Linux 上以 Python 3.13.18 實測。