一枚無人刻得出的印信
三千年來,要證明一封信出自你之手,靠的是用印章戒指壓出的火漆封印。它行得通,是因為戒指難以複製——直到有人偷走戒指,或刻出一枚以假亂真的仿品。手寫簽名也有同樣的毛病:手穩的偽造者能描摹你的字跡,而影印本和正本一樣「有效」。更糟的是,你的簽名在每份文件上看起來都一模一樣,所以它根本說不出你究竟同意了哪一頁。
數位簽章一次修好這兩個毛病。任何人都能驗證它、沒有人偽造得了它,而且它被焊死在你所簽的那一串確切位元組上——把訊息翻動一個位元,簽章就驗不過了。上一篇你打造了一組金鑰對:一把私鑰,本質上只是一個巨大的祕密數字 d,以及由它推導出的一把公鑰 Q。簽署用那個祕密數字,驗證用那個公開的點。祕密本身從不移動。
一枚簽章必須保證什麼
在談數學之前,先釘住我們真正想要的東西。一套好的簽章機制要同時交出三項保證:身分驗證(簽的人確實握有那把私鑰)、完整性(訊息在簽署後連一個位元組都沒被改動過),以及不可否認性(簽署者事後賴不掉,因為只有他的金鑰才生得出那枚簽章)。
你也許認得一個更簡單的工具:MAC(訊息驗證碼),雙方共用一個祕密、用它替訊息蓋章。但 MAC 沒辦法向第三方證明任何事——任何能驗證標籤的人,也都能偽造它,因為他們握有同一個祕密。公鑰簽章打破了這份對稱:驗證是公開的,偽造卻不是。這種「人人可驗」的特性,正是無信任(trustless)所立足的根基——成千上萬個素未謀面的節點,每一個都能各自獨立地檢查你的交易。
- 金鑰生成(KeyGen)→ 產生一組金鑰對 (d, Q):私鑰 d 留在你的錢包裡,公鑰 Q 可以公諸於世。
- 簽署 Sign(d, m) → 吃進你的私鑰與一段訊息 m,吐出一枚簽章 σ。只有握有 d 的人才跑得了這一步。
- 驗證 Verify(Q, m, σ) → 吃進公鑰、訊息與簽章,回傳真或假。任何人都能跑這一步;它從不碰到 d。
深入 ECDSA:簽署的那道方程式
比特幣與以太坊都用 ECDSA(橢圓曲線數位簽章演算法)來簽章,跑在你上一篇認識的 secp256k1 曲線上。回顧橢圓曲線密碼學的設定:曲線上有一個固定的基點 G,以及一個巨大的質數 n(約 1.158 × 10⁷⁷),稱為階——讓 G 跳 n 步,就會繞回起點。你的私鑰是 [1, n−1] 範圍內的一個祕密數字 d;你的公鑰是點 Q = d·G。要從 Q 反推出 d,等於要逆轉那次純量乘法,也就是橢圓曲線離散對數問題:被公認難到天文等級。
簽署並不是直接簽訊息——它簽的是訊息的雜湊(一個固定 32 位元組的摘要),所以不論你簽的是一個位元組還是一整份合約,成本都一樣。底下就是真正的演算法:
# secp256k1 parameters
G = base point; n = order of G # n is a 256-bit prime, ~1.158e77
d = private key # secret integer in [1, n-1]
Q = d * G # public key (a point on the curve)
function sign(message, d):
z = keccak256(message) # 32-byte hash, read as an integer
do:
k = secure_random(1, n-1) # the nonce: fresh, secret, single-use!
(x1, y1) = k * G # one scalar multiplication
r = x1 mod n
s = inverse(k, n) * (z + r * d) mod n # inverse() is modular inverse
while r == 0 or s == 0 # vanishingly rare; just retry
return (r, s) # the signature is the pair (r, s)簽章就只是兩個數字,*(r, s)*。第一個 r,是隨機點 k·G 的 x 座標;第二個 s,把雜湊 z、同一個 r 與你的私鑰 d 攪在一起,全程由那個祕密的一次性數值 k 拌勻。正因為 k 每次都是全新的,所以同一把金鑰簽同一段訊息兩次,會產生兩枚不同的簽章——而這是刻意設計的。
驗證:剛剛好成立的那套代數
現在換另一邊。驗證者手上有你的公鑰 Q、訊息,以及簽章 *(r, s)*——但永遠沒有 d 或 k。他們跑這一段:
function verify(message, (r, s), Q):
if not (1 <= r < n and 1 <= s < n): return false # reject junk
z = keccak256(message)
w = inverse(s, n) # w = s^{-1} mod n
u1 = (z * w) mod n
u2 = (r * w) mod n
(x1, y1) = u1 * G + u2 * Q # two scalar mults and one point add
return (x1 mod n) == r # valid iff the x-coordinate equals r
# Why it works (the whole point of the design):
# u1*G + u2*Q = u1*G + u2*(d*G) = (u1 + u2*d) * G
# u1 + u2*d = w*(z + r*d) = s^{-1}*(z + r*d)
# but s = k^{-1}*(z + r*d), so s^{-1}*(z + r*d) = k
# therefore the point is exactly k*G, whose x-coordinate is r. QED.把那段註解慢慢讀過,因為它是真的優美。驗證者把幾個公開數字組合起來,最後手上握著點 k·G——這個點只有簽署者建得出來,因為只有簽署者選了祕密的 k、也知道那把讓 s 驗得過的 d。這個重建出的點,其 x 座標必須等於 r。一個不知道 d 的偽造者,除非去猜一個 256 位元的數字,否則無法湊出落在正確點上的 *(r, s)*:毫無指望。
替一筆真實交易簽章
我們用一筆以太坊交易把它講具體。當你要轉 1 ETH 給朋友時,你的錢包把那些欄位(收款人、金額、nonce、gas、鏈 ID、data)湊齊、序列化,再對那串位元組跑 Keccak-256,得到一個 32 位元組的雜湊 z。ECDSA 簽的是這個雜湊——而不是原始交易。輸出打包成 *(r, s, v)*,其中多出來的那個位元組 v 是一個復原識別碼。
為什麼要多這一個位元組?因為光靠 *(r, s)* 和訊息,你其實就能反推出簽署它的那把公鑰——x 座標為 r 的候選點只有兩個,而 v 告訴你是哪一個。這是以太坊特有的小把戲(`ecrecover`),意思是交易根本不必夾帶你的公鑰:網路從簽章把它復原出來,再雜湊成你的位址(正是下一篇的主題)。合約也能在鏈上做同樣的檢查:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract VerifySig {
// n/2 for secp256k1 -- the EIP-2 "low-s" boundary (see next section)
uint256 constant HALF_N =
0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0;
/// Recover the address that signed `hash` from an (r, s, v) signature.
function signerOf(bytes32 hash, bytes32 r, bytes32 s, uint8 v)
external pure returns (address)
{
// Reject the malleable "high-s" twin signature (r, n - s)
require(uint256(s) <= HALF_N, "high-s");
require(v == 27 || v == 28, "bad v");
address signer = ecrecover(hash, v, r, s); // EVM precompile at 0x01
require(signer != address(0), "invalid signature");
return signer; // compare this to your expected signer
}
}在網路那一端,驗證你交易的節點會跑同樣的復原程序、確認復原出的位址與交易聲稱的 from 相符,再檢查 nonce 與餘額。沒有有效簽章,就不准花錢——沒有例外。這正是區塊鏈用來知道「你就是你」的精確機制:沒有銀行、沒有密碼檔、整個流程裡也沒有任何中央權威。
那個毀掉一台主機、又偷走比特幣的 nonce
回頭看簽署的程式碼,有一條規則凌駕一切:nonce k 必須隨機、保密、且絕不重用。違反它,你洩漏的就不只是一枚簽章——而是私鑰本身。2010 年,fail0verflow 團隊揭露 Sony 替每一份 PlayStation 3 韌體簽章時都用了固定的 k。兩枚簽章、一個固定的 k,主機的主簽署金鑰就掉了出來——Sony 永遠也撤銷不了它。2013 年 8 月,Android 的 `SecureRandom` 有個瑕疵,害幾個比特幣錢包生出重複的 k 值;竊賊掃描鏈上、找出兩筆共用同一個 r 的交易,復原出金鑰,把幣全捲走了。
# Two signatures by the SAME key that reused the SAME nonce k
# necessarily share the same r (because r = (k*G).x). Then:
#
# s1 = k^{-1} * (z1 + r*d)
# s2 = k^{-1} * (z2 + r*d)
# Subtract:
# s1 - s2 = k^{-1} * (z1 - z2)
#
# Solve for the secret nonce, then for the PRIVATE KEY:
k = (z1 - z2) * inverse(s1 - s2, n) mod n # recover k
d = (s1 * k - z1) * inverse(r, n) mod n # recover d <-- game over
# Anyone who watched the public chain can now sign as you, forever.可塑性,以及 ECDSA 之外的世界
ECDSA 還有一個值得誠實認識的怪癖。對任何有效簽章 *(r, s)* 而言,*(r, n − s)* 也是同一段訊息的有效簽章——你不需要私鑰就能翻動 s。這就是簽章可塑性:第三方能改動簽章位元組,使交易的雜湊改變,儘管交易本身原封未動。那些以原始交易雜湊為鍵的程式碼會被它搞糊塗;它在 2014 年 Mt. Gox 崩盤期間攪渾了鑑識。修法是 low-s 規則(比特幣的 BIP-62、以太坊的 EIP-2):只接受 *s ≤ n/2* 的那一半——正是上面合約裡的 `HALF_N` 檢查。要注意它從未讓任何人花掉你的幣;它只能改寫一個識別碼,這也是為什麼天真地拿交易雜湊來防重播,可能成為簽章重播的隱患。
早期比特幣選用 ECDSA,部分原因是更乾淨、更古老的 Schnorr 簽章在 2008 年前還受專利保護。Schnorr 的簽署方程式簡單到只是 s = k + e·d(其中 e 是公開點與訊息的雜湊)——而正因為它對金鑰與 nonce 是線性的,多位簽署者的金鑰與簽章可以相加成單一的聚合值。這驅動了比特幣的 Taproot(BIP-340,自 2021 年上線):一筆多簽支出在鏈上可以看起來像一枚普通簽章,既省空間又改善隱私。Schnorr 還擁有更乾淨的安全性證明,也沒有那個可塑的雙胞胎。
在聚合的最極端坐著 BLS 簽章,它建立在橢圓曲線配對之上。BLS 能把數千枚各自獨立的簽章壓縮成一枚短簽章,只需單一檢查就驗完——這正是以太坊的權益證明共識用它把每個時槽裡數萬筆驗證者證言摺疊起來的原因。代價講白了也很實在:BLS 每枚簽章的驗證較重,且仰賴配對友善的曲線,所以每套機制——ECDSA、Schnorr、BLS——都靠它各自最佳化的東西掙得一席之地。