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