背景: ECDSA ( Elliptic Curve Digital Signature Algorithm ) 可以说是 整个 web3/区块链/加密货币 行业 的安全性基石,被广泛运用于各种虚拟币转账的 签名。 无论是 ECC还是ECDSA, 其安全性保障在于 椭圆曲线上离散对数问题的求解困难性. 可以说要是哪天离散对数问题被攻破了, 相当多的虚拟币的安全性将无从谈起。 这篇文章致力于用最少的数学公式,带你直观理解 ECDSA, 希望你提前了解 ECC 椭圆曲线非对称加密算法( 可移步 加密货币安全基石:直观理解ECC椭圆曲线加密算法)
回顾一下 椭圆曲线上的 离散对数问题
给定一个 椭圆曲线起点G, 终点 Q, 大质数p, 要求找到满足下列条件的私钥 d 是非常困难的。
这就叫做椭圆曲线上的离散对数问题。 离散对数问题目前并无有效的攻击方法, 被认为是非常难以计算的数学问题, ECC 加密 和 ECDSA 签名的安全性都来源于这个 特性。
上面的式子省略 模运算 换一种写法就是这样, 给定一个椭圆曲线起点G, 私钥 d, 可以计算出终点 Q
但反之给定 终点 Q和起点 G, 很难计算出私钥 d
ECDSA 如何工作
ECDSA 签名
定义 G为椭圆曲线 起点, g 为循环群, 私钥 为 d, 公钥Q=dG, n 为 一个大质数, 被签名的原文为 M , 其中 G,Q,M,n 公开, d 为签名方严格保密 签名过程如下:
- 对原文进行取哈希操作, 令
- 从 范围内选一个随机数 k, 计算终点 , R 的 横坐标记为 r
- 计算 , 其中 为 的逆元
- 最终签名由两部分组成
直观理解签名公式
直观理解一下为什么签名公式这么设计, 可以从这几个方面考虑
-
为什么签名对象不是原文, 而是原文的哈希。 两个原因: 1是效率问题:直接对较长的原文进行签名会消耗更多的计算资源和时间。2是安全性问题:用大白话说就是加了一层哈希, 无论签名多少次, 攻击者都更难不到 原文和 签名的关联。
-
如何理解 z+rd 。原文的哈希是一个数字z, 很自然地想到 z+d , 为了隐藏私钥,最好在私钥签名加一个随机数 r, 于是变成了 z+rd.
-
如何理解 。 因为 是在椭圆有限域上做计算, 所以肯定是要取模操作映射到有限的值域上面, 为了抗碰撞, 为了保障签名的雪崩性, 最后得 对一个很大的质数取模。
-
如何理解 。 大白话说就是 为了防止攻击者看出来随机是 k 和签名之间的联系, 对 k 取 大质数 n 的逆元可以隐藏签名和随机数的关系, 进而隐藏背后的签名私钥d
ECDSA 验签
验签过程 只需要验证下面这个式子是否成立即可:
为什么上面式子成立, 就能验签成功呢? 我们不妨将上面的式子展开来多写几步
左边 , 右边将 提取出来, 得到:
两边忽略 G,得到:
两边同时取逆元, 得到:
这个式子是不是很熟悉, 没错 这个式子就是签名的式子。
签名需要用到 私钥d ,随机点P=kG 的横坐标r, 和 原文的哈希值 z。其中只有私钥 d为签名方保密, 其他参数全部公开。 而验证签名式子再写一遍
验签需要用到椭圆曲线G,原文的哈希值z, 签名 s, 随机点P=kG 的横坐标r, 公钥Q, 全部参数公开
私钥签名, 共钥验签 就是这个意思
ECDSA 安全性分析
假设攻击者需要伪造一个签名, 那么它需要伪造一把私钥 d', 随机数 k 和随机点横坐标 r 本身就是随机点所以无需伪造 , 此时的签名变成这样:
验签过程
注意验签到时候用到了Q, 攻击者可以伪造 私钥 d', 但是 Q 是公钥是公开的, Q 无法伪造, 除非攻击者用到的 伪造 私钥 d' 也满足 :
这就回到了椭圆曲线上的离散对数问题了, 给定一个终点Q和起点G, 是非常难以找到中间的系数 也就是私钥的。 反过来说, 如果验签成功, 那么签名方也就是公钥 Q 的持有方是无法抵赖的, 不能说我没有签过这个名, 因为除了有私钥的人可以签, 别人无法签名, 只有公钥匙持有能力签名。 签了就是签了, 一旦签名, 也就无法抵赖
使用 golang 进行 ECDSA 签名
golang 使用 ECDSA非常简单, crypto 里面有现成的包直接调用
package main
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"fmt"
)
func main() {
// generate pri key
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
fmt.Println("failed to generate ecdsa pri key, err:", err)
return
}
message := []byte("Hello, ECDSA!")
// compute hash value of plain text
hash := sha256.Sum256(message)
// sign with random number r and pri key
r, s, err := ecdsa.Sign(rand.Reader, privateKey, hash[:])
if err != nil {
fmt.Println("failed to sign err:", err)
return
}
// display sign
fmt.Println("sign result:")
fmt.Printf("r: %x\n", r)
fmt.Printf("s: %x\n", s)
// verify sign
verified := ecdsa.Verify(&privateKey.PublicKey, hash[:], r, s)
fmt.Println("sign verify result:", verified)
}
运行结果:
sign result:
r: a36e32c48995a2745c6faa911b73f95b192dd4aca51533b713f1598a743508da
s: ce7213cd9560792f55f3b0d3abb289f660b20dc6c90168d130c318b8cfab690f
sign verify result: true
总结
我们回顾了 椭圆曲线上的离散对数问题, 根据这个性质推广到ECDSA 签名 和 验签, 然后是 ECDSA 的安全性分析, 最后使用 golang 实现了 ECDSA的签名与验签。