本文将解释 ECDSA(Elliptic Curve Digital Signature Algorithm,椭圆曲线数字签名算法)是如何工作的,以及它为什么 有效。
在本教程中,我们将从第一性原理出发,逐步“重新发现”该算法。
先决条件
我们假定您已具备以下背景知识:
数字签名回顾
数字签名算法是一种协议,它使用户能够对字符串提供加密的认可印记。
这样的用户拥有一个 public key (公钥,对应于椭圆曲线点的一对 ( x , y ) (x,y) ( x , y ) )。该点是由一个 private key (私钥)生成的,私钥是一个秘密的标量值。
给定公钥、消息和 signature (签名),另一个用户可以验证该签名是由拥有该公钥对应私钥的人生成的,并且只有该人才能生成该签名。
例如,加密货币使用数字签名来验证用户确实想要进行交易。用户 Alice 可以向区块链发送一条消息,说“发送我的 10 个代币给 Bob”。区块链网络必须确定 Alice 确实对该消息给予了认可印记,并且 Bob 没有窃取 Alice 的代币。
生成的签名必须是专属于该消息的,而不能被其他消息接受。否则,攻击者可能会利用签名让受害者批准恶意交易。如果“转账 10 个代币给 Bob”这条消息的签名也能被用作对“转账 10 个代币给 Eve”这条消息的认可印记,那么 Eve 就可以利用 Bob 转账的签名来窃取 Alice 的代币。
我们将生成签名的人称为 prover (证明者)或 signer (签名者),将测试 (公钥, 消息, 签名) 元组是否有效的人称为 verifier (验证者)。
从本质上讲,椭圆曲线数字签名是对私钥知识的证明。具体来说,它是对椭圆曲线点的离散对数知识的证明。
对于那些已经见过 ECDSA 算法的读者:
r = k G s = k − 1 ( h + r p ) r = ? s − 1 ( h G + r P ) \begin{align*}
r &= kG\\
s &= k^{-1}(h + rp)\\
r &\stackrel{?}= s^{-1}(hG + rP)
\end{align*} r s r = k G = k − 1 ( h + r p ) = ? s − 1 ( h G + r P )
如果你不知道这些公式是怎么来的,你将会学到推导它们背后的思路。
术语和符号
我们用大写字母(如 Q Q Q )来表示椭圆曲线点。生成元将被称为 G G G 。G G G 的值对所有相关方都是已知的。标量和点之间的乘法写成 a G aG a G ,这意味着将 G G G 与自身相加 a a a 次。
给定两点 P P P 和 Q Q Q ,如果某人知道 x x x 使得 x P = Q xP = Q x P = Q ,或者知道 y y y 使得 P = y Q P = yQ P = y Q ,我们称他们知道 P P P 和 Q Q Q 之间的离散对数关系 (discrete log relationship)。
如果某人知道一个标量 q q q 使得 Q = q G Q = qG Q = qG ,我们称他们知道点 Q Q Q 的离散对数 。
除非另有说明,我们用相同字母的小写形式来表示某个点的离散对数。
所有算术运算都是在一个 finite field 中进行的,但为了简洁起见,我们省略了 ( m o d p ) \pmod p ( mod p ) 符号。
失败的数字签名尝试
朴素方法:泄露私钥
我们想证明我们知道公钥 P P P 的私钥 p p p ,其中 P = p G P = pG P = pG 。
最朴素的方法是将私钥 p p p 泄露给验证者,以便验证者可以检查确实存在 P = p G P = pG P = pG 。
当然,这违背了对 p p p 保密的初衷。
证明知道 P P P 和 Q Q Q 之间的离散对数关系
证明我们知道 p p p ,等价于证明我们知道生成元 G G G 和公钥 P P P 之间的离散对数关系。也就是说,G G G 与自身相加 p p p 次得到 P P P 。泄露 G G G 和 P P P 之间的离散对数关系就会泄露私钥。
因此,我们可以不证明我们知道生成元 G G G 和公钥 P P P 之间的离散对数关系,而是证明我们知道 P P P 和 Q Q Q 之间的离散对数关系,其中 Q Q Q 由证明者选择,且它的离散对数 q q q 对验证者来说是未知的。如果验证者不知道 Q Q Q 的离散对数,那么给定 P P P 和 Q Q Q 之间的离散对数关系,验证者也无法推导出私钥。
设 s s s 为 P P P 需要与自身相加多少次才能产生 Q Q Q ;换句话说,s P = Q sP = Q s P = Q 。如果证明者分别知道 P P P 和 Q Q Q 的离散对数为 p p p 和 q q q ,他们可以轻松计算出 s = q / p s = q/p s = q / p 。
s = q / p s = q/p s = q / p 的推导如下:
Q = s P Q = q G P = p G q G = s p G q = s p q p = s \begin{align*}
Q &= sP\\
Q &= qG\\
P &= pG\\
qG &= spG \\
q &= sp \\
\frac{q}{p} &= s\\
\end{align*} Q Q P qG q p q = s P = qG = pG = s pG = s p = s
然后验证者可以检查确实有 Q = s P Q = sP Q = s P ,这就表明证明者知道 P P P 和 Q Q Q 之间的离散对数关系。
然而,这种方法失败了,因为恶意的证明者可以拿走别人的 P P P (他们不知道其私钥),选择一个随机值 s s s ,计算 Q = s P Q = sP Q = s P ,然后将 ( P , Q , s ) (P, Q, s) ( P , Q , s ) 发送给验证者。
因此,证明者证明他们知道 P P P 和 Q Q Q 之间的离散对数关系,并不足以证明他们知道 P P P 本身的离散对数。它仅仅表明他们将某个 P P P 乘以 s s s 从而生成了 Q Q Q 。
防止随机选择的 Q Q Q
上面的方法失败是因为证明者可以在实际上不知道 Q Q Q 本身离散对数的情况下提供 Q Q Q 。
如果为了确保证明者知道 Q Q Q 的离散对数,证明者向验证者泄露 q q q (即 Q Q Q 的离散对数)会怎样?
在这种情况下,证明者必须将 q q q 和 s s s 都泄露给验证者。然后,s s s 证明了证明者知道需要将 P P P 与自身相加多少次才能得到 Q Q Q ,而 q q q 证明了证明者知道 Q Q Q 的离散对数。提供 q q q 证明了 Q Q Q 不是通过任意选择一个值 s s s 并将 P P P 与自身相加 s s s 次来选定的。
在这种情况下,验证者检查:
q G = ? Q // 证明者知道 Q 的离散对数 s P = ? Q // 证明者知道需要将 P 与自身相加多少次才能得到 Q \begin{align*}
qG &\stackrel{?}{=} Q &&\text{ // 证明者知道 Q 的离散对数}\\
sP &\stackrel{?}{=} Q &&\text{ // 证明者知道需要将 P 与自身相加多少次才能得到 Q}
\end{align*} qG s P = ? Q = ? Q // 证明者知道 Q 的离散对数 // 证明者知道需要将 P 与自身相加多少次才能得到 Q
有了这些检查,证明者在不知道 Q Q Q 的离散对数和 P P P 的离散对数的情况下,就无法创建或生成有效的关系。
然而,验证者可以基于这些信息计算出私钥。
Q = s P s − 1 Q = P s − 1 ( q G ) = P s − 1 q = P / G s − 1 q = p \begin{align*}
Q &= sP\\
s^{-1}Q&=P\\
s^{-1}(qG)&=P\\
s^{-1}q &= P/G\\
s^{-1}q &= p\\
\end{align*} Q s − 1 Q s − 1 ( qG ) s − 1 q s − 1 q = s P = P = P = P / G = p
所以我们陷入了困境:如果我们泄露 Q Q Q 的离散对数,验证者就能计算出私钥。如果证明者可以随意选择 Q Q Q ,那么证明者就可以为他们不知道离散对数的公钥创建签名。
我们绝不能让验证者能够计算出私钥,所以泄露 q q q 是绝对不行的。
因此,我们的解决方案必须包含由证明者挑选 Q Q Q 的环节——但我们必须防止证明者能够完全任意地挑选 Q Q Q (例如通过选择任意的 s s s 并生成 Q = s P Q = sP Q = s P )。
继续往下看,证明者必须证明他们知道 Q Q Q 的离散对数和 P P P 的离散对数,但又不能泄露任何一个离散对数 p p p 或 q q q 。
事实证明,证明我们知道两个 点的离散对数而不泄露它们,比证明我们知道一个 点的离散对数而不泄露这单一的离散对数要容易得多。
如果证明者知道 P P P 和 Q Q Q 的离散对数,那么他们还必须知道什么?
核心思想是这样的:
如果证明者知道 P P P 和 Q Q Q 的离散对数,那么他们不仅必须知道满足 s P = Q sP = Q s P = Q 的 s s s ,还必须知道满足 s ′ ( h G + P ) = Q s'(hG + P) = Q s ′ ( h G + P ) = Q 的 s ′ s' s ′ ,其中 h h h 是由验证者选择的任意值,且是证明者无法预测的值。
换句话说,我们将开始在 P P P 和 Q Q Q 之间的关系中引入加法和乘法“偏移”(shifts)——如果证明者知道 P P P 和 Q Q Q 的离散对数,那么只要证明者知道偏移量 h h h 是多少,他们就必须能够计算出 s ′ s' s ′ 使得 s ′ ( h G + P ) = Q s'(hG + P) = Q s ′ ( h G + P ) = Q 。
加法偏移可防止伪造
为了防止证明者选择的 Q Q Q 只是 P P P 的简单标量乘法,验证者可以注入一个加法偏移 h h h 。
在证明者将 P P P 和 Q Q Q 发送给验证者之后,验证者回复 h h h (一个公开的标量值)。证明者必须随后提供 s s s 使得 Q = s ( h G + P ) Q = s(hG + P) Q = s ( h G + P ) (我们不再需要 s ′ s' s ′ 符号了)。
现在证明者无法控制 P P P 和 Q Q Q 之间的离散对数关系,他们必须改为证明自己知道 ( h G + P ) (hG + P) ( h G + P ) 和 Q Q Q 之间的离散对数关系。
由于验证者已经拥有了 P P P 和 Q Q Q ,证明者不能仅仅挑选一个随机的 s s s 并生成满足 Q = s ( h G + P ) Q = s(hG + P) Q = s ( h G + P ) 的 Q Q Q 。证明者必须证明他们知道新点 ( h G + P ) (hG + P) ( h G + P ) 和原始 Q Q Q 之间的离散对数关系。
如果证明者知道 P P P 和 Q Q Q 的离散对数,那么很容易计算出 s s s 为:
s = q h + p s = \frac{q}{h + p} s = h + p q
具体来说,证明者解出了:
s ( h G + P ) = Q s ( h + p ) = q s = q h + p \begin{align*}
s(hG + P)&=Q\\
s(h + p) &= q\\
s&=\frac{q}{h+p}
\end{align*} s ( h G + P ) s ( h + p ) s = Q = q = h + p q
让我们总结一下刚刚创建的交互式算法:
我们假设双方都同意使用 G G G (及其所属的椭圆曲线群)。
证明者发布他们的公钥 P P P 并希望证明自己知道私钥 p p p 。
验证者以一个标量 h h h 作为响应。
证明者挑选一个随机的 q q q 并计算 Q = q G Q = qG Q = qG 。
证明者计算 s = q ( h + p ) − 1 s = q(h + p)^{-1} s = q ( h + p ) − 1 并将 ( s , Q ) (s, Q) ( s , Q ) 发送给验证者。
验证者检查 Q = ? s ( h G + P ) Q \stackrel{?}= s(hG + P) Q = ? s ( h G + P ) 。
最后一步的检查之所以有效,是因为在底层运算中,s s s 抵消了 h h h 和 p p p 的离散对数:
Q = s ( h G + P ) q G = q h + p ( h G + P ) q G = q h + p ( h G + p G ) q G = q h + p ( h + p ) G q G = q h + p ( h + p ) G q G = q G \begin{align*}
Q &= s(hG + P)\\
qG &= \frac{q}{h + p}(hG+P)\\
qG &= \frac{q}{h + p}(hG+pG)\\
qG &= \frac{q}{h + p}(h+p)G\\
qG &= \frac{q}{\cancel{h + p}}(\cancel{h+p})G\\
qG &= qG
\end{align*} Q qG qG qG qG qG = s ( h G + P ) = h + p q ( h G + P ) = h + p q ( h G + pG ) = h + p q ( h + p ) G = h + p q ( h + p ) G = qG
抵御伪造签名
让我们看看恶意的证明者将如何试图在不知道 P P P 和 Q Q Q 离散对数的情况下去计算 s s s ,并看看为什么这种尝试会失败。
证明者捏造了一个值 q ~ \tilde{q} q ~ 并生成 Q = q ~ P Q = \tilde{q}P Q = q ~ P ,然后将 Q Q Q 发送给验证者。设 q ~ \tilde{q} q ~ 是由恶意证明者捏造的值,而不是 Q Q Q 的离散对数。
验证者回复标量 h h h 。
恶意的证明者现在必须求解方程式:
s ( P + h G ) = Q s(P + hG) = Q s ( P + h G ) = Q
由于证明者知道 Q = q ~ P Q = \tilde{q}P Q = q ~ P (但不知道 p p p ),他们可以尝试两种代入方式:
Q = q ~ P Q = \tilde{q}P Q = q ~ P
P = q ~ − 1 Q P = \tilde{q}^{-1}Q P = q ~ − 1 Q
然而,无论证明者代入的是 Q Q Q 还是 P P P ,他们都会遇到一个问题:由于不知道 P P P 的离散对数,他们无法解出 s s s 。
代入 Q Q Q
在这里,证明者代入 Q Q Q :
s ( P + h G ) = Q s(P + hG) = \boxed{Q} s ( P + h G ) = Q
s ( P + h G ) = q ~ P s(P + hG) = \boxed{\tilde{q}P} s ( P + h G ) = q ~ P
s = q ~ P P + h G s = \frac{\tilde{q}P}{P + hG} s = P + h G q ~ P
我们不能对椭圆曲线点进行除法运算,因此为了计算上述分数,我们需要知道 p p p :
s = q ~ p p + h s = \frac{\tilde{q}p}{p + h} s = p + h q ~ p
但是证明者不知道 p p p ,所以他们无法计算 s s s 。
代入 P P P
现在恶意证明者尝试代入 P P P 而不是 Q Q Q ,但又遇到了由于不知道 Q Q Q 离散对数而产生的问题:
s ( q ~ − 1 Q + h G ) = Q s(\tilde{q}^{-1}Q + hG) = Q s ( q ~ − 1 Q + h G ) = Q
s = Q ( q ~ − 1 Q + h G ) s = \frac{Q}{(\tilde{q}^{-1}Q + hG)} s = ( q ~ − 1 Q + h G ) Q
现在恶意证明者需要知道 Q Q Q 的离散对数才能计算上述公式。然而,证明者不知道 Q Q Q 的离散对数,他们只知道它是 q ~ P \tilde{q}P q ~ P ,但不知道 p p p 。因此,恶意证明者无法生成 s s s 。
为什么偏移必须是加法
我们怎么知道要对 P P P 加上 h G hG h G ,而不是乘以 h P hP h P 并要求证明者得出 s s s 使得 Q = h s P Q = hsP Q = h s P 呢?
问题在于,如果使用乘法偏移,证明者就能抵消掉 Q Q Q 和 P P P 的离散对数因子。
回顾一下:恶意的证明者不知道 Q Q Q 的离散对数 q q q ,也不知道 P P P 的离散对数 p p p 。然而,他们确实知道 Q Q Q 比 P P P 大 q ~ \tilde{q} q ~ 倍,其中 q ~ \tilde{q} q ~ 是他们捏造的数字,并非真实的离散对数 q q q 。
如果验证者给出了 h h h ,并要求证明者算出一个 s s s 使得 Q = s h P Q = shP Q = s h P ,那么证明者只需计算 s = q ~ / h s = \tilde{q}/h s = q ~ / h 。
这将通过验证者的测试:
Q = s h P Q = q ~ h h P Q = q ~ h h P Q = q P \begin{align*}
Q &= shP \\
Q &= \frac{\tilde{q}}{h}h P\\
Q &= \frac{\tilde{q}}{\cancel{h}}\cancel{h} P\\
Q &= qP \\
\end{align*} Q Q Q Q = s h P = h q ~ h P = h q ~ h P = qP
因此,协议中必须包含加法偏移,以便在第一个点 (P + h G P + hG P + h G ) 和第二个点 Q Q Q 之间不存在简单的乘法关系 s h sh s h 。
让我们的协议变为非交互式
不幸的是,我们的解决方案要求验证者在接收到 P P P 和 Q Q Q 之后发送 h h h ,这要求证明者和验证者彼此进行交互 。
如果我们想让我们的协议变成非交互式,那么 h h h 就不能来自验证者,必须由证明者来生成它。
如果证明者事先知道 h h h (如果证明是非交互式的,这是必然的),那么我们又回到了最初的问题。
在这种情况下,恶意证明者可以随机挑选 h h h ,然后随机挑选 s s s ,并计算
Q = s ( h G + P ) Q = s(hG + P) Q = s ( h G + P )
然后将 ( P , Q , s , h ) (P, Q, s, h) ( P , Q , s , h ) 发送给验证者。验证者不知道 Q Q Q 是否是由一个随机的 s s s 生成的。
为了避免由证明者选择 h h h 以及与验证者的交互,我们可以通过对 Q Q Q 进行哈希运算来模拟验证者选择 h h h 的过程:
h = h a s h ( Q ) h = \mathsf{hash}(Q) h = hash ( Q )
既然验证者反正也是随机挑选 h h h 的,证明者完全可以使用哈希函数以伪随机的方式计算出 h h h 。这种技术被称为 Fiat-Shamir Transform。
一个对私钥知识的有效证明
算法如下:
证明者挑选一个随机标量 q q q 并计算 Q = q G Q = qG Q = qG 。
证明者计算 h = h a s h ( Q ) h = \mathsf{hash}(Q) h = hash ( Q ) 。
证明者计算 s = q h + p s = \frac{q}{h + p} s = h + p q 。
证明者将 ( P , Q , s , h ) (P, Q, s, h) ( P , Q , s , h ) 发送给验证者。
验证者计算 h = h a s h ( Q ) h = \mathsf{hash}(Q) h = hash ( Q ) 。
验证者检查 Q = s ( h G + P ) Q = s(hG + P) Q = s ( h G + P ) 。
这之所以是安全的,是因为恶意的证明者在不知道 Q Q Q 的离散对数和 P P P 的离散对数的情况下,无法执行第 3 步。
伪造是不可能的
假设恶意证明者挑选了 q ~ \tilde{q} q ~ 并生成 Q = q ~ G Q = \tilde{q}G Q = q ~ G 。在这里,证明者知道 Q Q Q 的离散对数,但不知道 P P P 的离散对数。在对 Q Q Q 进行哈希得到 h h h 之后,恶意证明者需要计算出满足 Q = s ( h G + P ) Q = s(hG + P) Q = s ( h G + P ) 的 s s s 。然而,他们做不到这一点,因为他们不知道 ( h G + P ) (hG + P) ( h G + P ) 的离散对数,毕竟 P P P 的离散对数对他们来说是未知的。
至此,我们已经展示了一种安全的算法,用于在不泄露私钥的情况下证明对私钥知识的掌握。
ECDSA 的前置算法
我们上面介绍的算法仅仅证明了对私钥知识的掌握,它不允许签名者对一条消息进行签名。
在 ECDSA 中,签名是一种证明,证明我们知道某个点的离散对数,该点是通过将我们的公钥加上 h G hG h G 生成的,其中 h h h 是消息的哈希值与另一点 Q Q Q 的离散对数。
然而,目前的设计引入了一个安全漏洞,因为证明者可以首先 计算 h = h a s h ( message ) h = \mathsf{hash}(\text{message}) h = hash ( message ) ,然后挑选一个随机的 s ′ s' s ′ 并计算 Q = s ′ ( h G + P ) Q = s'(hG + P) Q = s ′ ( h G + P ) 。此时 Q Q Q 再次完全受证明者控制,所以我们无法相信证明者真的知道离散对数 q q q 。
为了确保安全,我们再次需要引入一些不受证明者控制的额外的伪随机性。
添加随机性 r r r
设 r r r 是从对 Q Q Q 进行哈希推导出的随机值。如果我们按照如下方式将 r r r 乘上 P P P ,我们就能得到一个安全的数字签名算法。起初,这看起来像是制造了一个循环依赖,但这实际上是让算法变得安全的核心所在。证明者证明他们知道 ( r P + h G ) (rP + hG) ( r P + h G ) 和 Q Q Q 离散对数的算法如下:
Q = s ( h G + r P ) Q = s(hG + rP) Q = s ( h G + r P )
证明者随机挑选一个 p p p ,并将其公钥 P P P 作为 P = p G P = pG P = pG 发布。
证明者挑选一个随机标量 q q q 并计算 Q = q G Q = qG Q = qG 。
证明者挑选一个消息字符串 message \text{message} message 并计算 h = h a s h ( message ) h = \mathsf{hash}(\text{message}) h = hash ( message ) 。
证明者计算 r = h a s h ( Q ) r = \mathsf{hash}(Q) r = hash ( Q ) 。
证明者计算 s = q ⋅ ( h + r p ) − 1 s = q\cdot(h + rp)^{-1} s = q ⋅ ( h + r p ) − 1 。
证明者将 ( message , Q , s ) (\text{message}, Q, s) ( message , Q , s ) 发送给验证者。
验证者计算 r ′ = h a s h ( Q ) r' = \mathsf{hash}(Q) r ′ = hash ( Q ) 。
验证者计算 h = h a s h ( message ) h = \mathsf{hash}(\text{message}) h = hash ( message ) 。
验证者检查 Q = ? s ( h G + r ′ P ) Q \stackrel{?}= s(hG + r'P) Q = ? s ( h G + r ′ P ) 。
尽管证明者可以挑选任意的 h h h ,但他们无法控制点 ( h G + r P ) (hG + rP) ( h G + r P ) 的离散对数,因为 r r r 是通过哈希 Q Q Q 生成的。因此,为了计算出 s s s ,也就是 Q Q Q 和 ( h G + r P ) (hG + rP) ( h G + r P ) 之间的离散对数关系,证明者必须真实地知道 P P P 的离散对数 p p p 。
优化 ECDSA 前置算法
优化 1:哈希 Q Q Q 是不必要的,因为椭圆曲线乘法本身表现得就像一个哈希函数
让我们回想一下 r = h a s h ( Q ) r = \mathsf{hash}(Q) r = hash ( Q ) 试图达成的目的。最终的验证公式是:
Q = ? s ( h G + r P ) Q \stackrel{?}= s(hG + rP) Q = ? s ( h G + r P )
核心思想是我们希望 r r r 依赖于 Q Q Q ,这样 Q Q Q 就不会完全由证明者可完全控制的值 s s s 决定。为了计算 s s s ,证明者必须知道 P P P 的离散对数。
我们需要寻找一种比哈希更廉价的替代方案来让 r r r 依赖于 Q Q Q 。
考虑到如果 r r r 依赖于 Q Q Q ,那么它也依赖于 Q Q Q 的离散对数 q q q ,因为 q q q 完全决定了 Q Q Q 。
现在想想看,如果给定我们一个标量值 a a a ,由 A = a G A = aG A = a G 产生的椭圆曲线点 A A A 看起来是随机的。A A A 和 a a a 之间没有明显的关系。这种明显关系的缺失正是计算离散对数困难的原因所在。
哈希函数的输出同样看起来也是随机的:哈希的输入和输出之间没有明显的关系。
因此,我们可以像处理 h a s h ( q ) \mathsf{hash}(q) hash ( q ) 一样来处理 Q = q G Q = qG Q = qG 。也就是说,Q Q Q 本身表现得就像哈希的输出。然而,我们不能直接设定 r = Q r = Q r = Q ,因为它们类型不同。相反,我们只需取 Q Q Q 的 x x x 值并让它成为 r r r (记住,Q Q Q 是一个 ( x , y ) (x, y) ( x , y ) 点)。
现在 r r r 本质上就是 q q q 的哈希,且因为 Q Q Q 直接依赖于 q q q ,r r r 表现得就像是 Q Q Q 的哈希。
因此,我们不计算 r = h a s h ( Q ) r = \mathsf{hash}(Q) r = hash ( Q ) ,而是设定 r = Q . x r = Q.x r = Q . x (意味着取点 Q Q Q 的 x x x 值)。
优化 2:我们不需要发送整个点 Q Q Q ,只需要发送 Q Q Q 的 x x x 值
椭圆曲线点由两个标量组成:点的 x x x 和 y y y 值。每一个 x x x 值只对应两个可能的 y y y 值,即 x 3 + b m o d p \sqrt{x^3 + b} \mod p x 3 + b mod p 的解。
因此,没有必要发送 Q Q Q ,只需发送 r r r ,因为 r r r 就是 Q Q Q 的 x x x 值。
完整的 ECDSA 算法:分步指南
我们现在展示标准的 ECDSA 算法,并附上最常用的符号表达。我们会指出我们之前使用的符号在哪里存在差异。
证明者随机挑选一个 p p p ,并将其公钥 P P P 作为 P = p G P = pG P = pG 发布。
证明者挑选一条他们想要签名的消息,并对其进行哈希处理得到 h = h a s h ( message ) h = \mathsf{hash}(\text{message}) h = hash ( message ) 。
证明者挑选一个随机标量 k k k (也就是我们之前一直称作的 q q q ,但文献中称之为 k k k )并计算 R = k G R = kG R = k G 。同样地,我们之前称之为 Q Q Q 的点,文献中称之为 R R R 。只有 R R R 的 x x x 值会被保留,记作 r = R . x r = R.x r = R . x 。
证明者为 R = s − 1 ( h G + r P ) R = s^{-1}(hG + rP) R = s − 1 ( h G + r P ) 计算 s s s ,公式如下:
s = h + r p k s = \frac{h + rp}{k} s = k h + r p
注意,s s s 与我们先前的实现相比是“倒置”(inverted)的。在实际的 ECDSA 算法中,证明者计算 s − 1 s^{-1} s − 1 ,而验证者稍后会对 s s s 求逆,正如我们将看到的那样。
签名者将 ( P , h , r , s ) (P, h, r, s) ( P , h , r , s ) 发送给验证者。
验证者计算 R ′ = s − 1 ( h G + r P ) R' = s^{-1}(hG + rP) R ′ = s − 1 ( h G + r P ) 并检查 R ′ R' R ′ 的 x x x 值是否等于 r r r 。
该公式在底层的运算逻辑如下:
= s − 1 ( h G + r P ) = k h + r p ( h G + r P ) = k h + r p ( h G + r p G ) = k h + r p ( h + r p ) G = k h + r p ( h + r p ) G = k G \begin{align*}
&=s^{-1}(hG + rP)\\
&=\frac{k}{h + rp}(hG + rP)\\
&=\frac{k}{h + rp}(hG + rpG)\\
&=\frac{k}{h + rp}(h + rp)G\\
&=\frac{k}{\cancel{h + rp}}(\cancel{h + rp})G\\
&=kG
\end{align*} = s − 1 ( h G + r P ) = h + r p k ( h G + r P ) = h + r p k ( h G + r pG ) = h + r p k ( h + r p ) G = h + r p k ( h + r p ) G = k G
这产生了相同的点 R R R ,因此 r r r 将与计算出的点 s − 1 ( h G + r P ) s^{-1}(hG + rP) s − 1 ( h G + r P ) 的 x x x 值相匹配。
给定签名推导公钥
以太坊和比特币区块链并不在给定公钥、消息和签名的情况下验证签名。相反,给定消息和签名,它们会求解出公钥,并检查该公钥是否与预期公钥相匹配。
要了解这是如何运作的,我们可以对验证公式做一些代数运算,从而在给定签名和消息哈希的情况下推导出公钥。
R = s − 1 ( h G + r P ) R = s − 1 h G + s − 1 r P R − s − 1 h G = s − 1 r P s r − 1 R − r − 1 h G = P \begin{align*}
R &= s^{-1}(hG + rP)\\
R &= s^{-1}hG + s^{-1}rP\\
R - s^{-1}hG &= s^{-1}rP\\
sr^{-1}R-r^{-1}hG&=P\\
\end{align*} R R R − s − 1 h G s r − 1 R − r − 1 h G = s − 1 ( h G + r P ) = s − 1 h G + s − 1 r P = s − 1 r P = P
然而,仅凭 ( message , r , s ) (\text{message}, r, s) ( message , r , s ) 是不够的,因为 r r r 对应于两个可能的点,这源于 r = R . x r = R.x r = R . x 存在两个 y y y 值。为了消除歧义,签名者还需要发送一个布尔变量来指明使用了哪个 y y y 值。发送完整的 y y y 值会占用更多空间。
给定签名“恢复”公钥的 ECDSA 算法如下:
证明者将其公钥 P P P 作为 P = p G P = pG P = pG 发布。
证明者挑选一条他们想要签名的消息 message \text{message} message ,并对其进行哈希处理得到 h = h a s h ( message ) h = \mathsf{hash}(\text{message}) h = hash ( message ) 。
证明者挑选一个随机标量 k k k 并计算 R = k G R = kG R = k G 。
证明者计算 h = h a s h ( message ) h = \mathsf{hash}(\text{message}) h = hash ( message ) 。
证明者在 R = s − 1 ( h G + r P ) R = s^{-1}(hG + rP) R = s − 1 ( h G + r P ) 中求解 s s s ,公式为 s = h + r p k s = \frac{h + rp}{k} s = k h + r p 。
签名者向验证者发送 ( message , r , s , v ) (\text{message}, r, s, v) ( message , r , s , v ) ,其中 v v v 是一个布尔值,用于指示使用了 r r r 的哪个 y y y 值。
验证者根据 v v v 和 r r r 推导出 R R R 。
验证者推导出公钥 P P P ,公式为:
P = s r − 1 R − r − 1 h G P = sr^{-1}R - r^{-1}hG P = s r − 1 R − r − 1 h G
如果计算出的 P P P 与证明者发布的公钥 P P P 相匹配,则接受该签名。
滥用 ECDSA 的潜在漏洞
ECDSA 中的延展性
给定签名 ( message , r , s , v ) (\text{message}, r, s, v) ( message , r , s , v ) ,攻击者可以计算出第二个签名 ( message , r , s ′ , v ′ ) (\text{message}, r, s', v') ( message , r , s ′ , v ′ ) ,使得 v ≠ v ′ v \neq v' v = v ′ 且 s ≠ s ′ s \neq s' s = s ′ ,而它依然能恢复出相同的公钥 P P P 。
攻击者只需计算 s s s 的加法逆元 s ′ = s − 1 s' = s^{-1} s ′ = s − 1 并翻转 v v v 的值,使得 R R R 变为其加法逆元。本质上,我们将 s s s 替换为 − s -s − s ,将 R R R 替换为 − R -R − R 。由于这两个值乘在一起,-1 会相互抵消,所以恢复出来的公钥是一样的:
P = ( − s ) r − 1 ( − R ) − r − 1 h G = s r − 1 R − r − 1 h G \begin{align*}P &= (-s)r^{-1}(-R) - r^{-1}hG\\
&= sr^{-1}R - r^{-1}hG
\end{align*} P = ( − s ) r − 1 ( − R ) − r − 1 h G = s r − 1 R − r − 1 h G
为了防止这种攻击,验证者不应接受同一消息曾出现过的不同签名。更通用、更稳健的解决方案是使用 nonce,即证明者必须包含在消息中的一个不断递增的数字。nonce(代表“number used once”,即只使用一次的数字)充当每个签名的唯一标识符。通过要求证明者在每个签名中合并一个全新且更大的 nonce,验证者可以轻松检测并拒绝重用或修改过往签名的尝试。
如果验证者未对消息进行哈希处理,就可以创建伪造证明。
让我们考虑一种情况:攻击者可以创建一个我们不知道离散对数的值 R R R ,但我们知道它的组成部分 。例如,攻击者挑选随机值 a a a 和 b b b 并计算 R = a G + b P R = aG + bP R = a G + b P ,以及 r = R . x r = R.x r = R . x 。
然后,攻击者计算 s = r / b s = r/b s = r / b 和 h = a r / b h = ar / b h = a r / b 。
我们可以通过将伪造的 s s s 和 h h h 值代入验证公式,来看看这种攻击是如何运作的:
R = ? s − 1 ( h G + r P ) R = ? ( r b ) − 1 ( ( a r b ) G + r P ) R = ? ( b r ) ( ( a r b ) G + r P ) R = ? ( a G + b P ) \begin{align*}
R &\stackrel{?}= s^{-1}(hG + rP) \\
R &\stackrel{?}= (\frac{r}{b})^{-1}((\frac{ar}{b})G + rP) \\
R &\stackrel{?}= (\frac{b}{r})((\frac{ar}{b})G + rP) \\
R &\stackrel{?}= (aG + bP)
\end{align*} R R R R = ? s − 1 ( h G + r P ) = ? ( b r ) − 1 (( b a r ) G + r P ) = ? ( r b ) (( b a r ) G + r P ) = ? ( a G + b P )
防御这种攻击的方法很简单:h h h 必须是验证者对消息进行哈希运算的结果。当一条消息被哈希时,哈希值恰好等于 a r / b ar/b a r / b 的可能性微乎其微。
总结
ECDSA 算法是一种对离散对数关系的知识证明,这种关系存在于任意点 R R R 和一个特定的点之间,后者代表了消息哈希乘上生成元再加上公钥,同时公钥还乘上了签名者无法控制的伪随机值。
具体来说,它是对 R R R 和 ( h G + r P ) (hG + rP) ( h G + r P ) 之间离散对数关系的知识证明。
尽管签名者可以任意挑选 R R R ,但他们无法控制点 ( h G + r P ) (hG + rP) ( h G + r P ) 的离散对数,因为 r r r 伪随机地依赖于 R R R 。签名者能够在 R = s ( h G + r P ) R = s(hG + rP) R = s ( h G + r P ) 中计算出 s s s 的唯一途径,是他们实际上确实知道 P P P 的离散对数,即私钥。
鸣谢
在撰写本文的过程中,我们参考了以下资源: