ECC 椭圆曲线加解密算法
一、为什么叫椭圆曲线
首先回忆一下直线方程式 y=ax+b ,在坐标系中表示一条直线,是一次方程,圆锥曲线可以用二次方程表示。椭圆曲线是用三次方程表示,如下:
其中,a 和 b 的取值不同,椭圆曲线的形状会有所改变,经典的形状如下图所示:
这时有读者会有疑问了,“上图中不是一个椭圆的形状啊,为什么叫椭圆曲线啊?”,原因是椭圆曲线的 3 次方程式和椭圆周长公式中的一部分很像,下面是椭圆周长公式:
可以看出积分公式中分母的平方恰恰就是椭圆曲线的公式。
椭圆曲线有以下两个特点:
- 画一条直线跟椭圆曲线相交,它们最多有三个交点;
- 关于 X 轴对称。
二、比特币使用的椭圆曲线
比特币中使用的椭圆曲线是 secp256k1,上文图中的椭圆曲线是连续的,但在像比特币这些实际应用中,都是有限域上定义的离散曲线。
椭圆曲线三次方程式中参数 a, b,基点 G 的不同,椭圆曲线的加密性能也不一样。一些常用的使用特定参数的椭圆曲线都有特定的标识,现在世界上比较主流的有:
- 美国国家标准与技术研究院(NIST)和美国国家安全局(NSA)的 secp256r1、secp521r1 等椭圆曲线
- 中国国家密码局认定的 SM2 国产密码算法。
- 比特币和以太坊使用的 secp256k1。
下面来看一下 secp256k1 的所有参数和方程式: y^2 = x^3 + 7 ( a = 0,b = 7)
选择的基点是:
可以看出方程式和参数并不复杂,反而简单,那么有读者要问了“这么简单的方程式和参数,那这个加密算法还安全吗?”,这个问题非常好,下面会讲到它的安全性其实并不是由方程式和参数决定的,而是由椭圆曲线上的运算决定的,请继续往下看。
三、椭圆曲线运算法则
在椭圆曲线上会定义两种运算,加法和乘法,但是其实它们和通常意义上的加法和乘法不一样的,只是为了方便理解所以这么叫,怎么不一样呢,下面看看具体的定义。
1. 椭圆曲线加法
根据上面介绍的椭圆曲线的特性“画一条直线跟椭圆曲线相交,它们最多有三个交点”,可以进行以下定义:
- 假设椭圆曲线上有 P、Q 两个点,经过这两个点做一条直线和椭圆曲线相交于第三点 R,然后做关于 x 轴的对称点 -R,-R 即是 R 的逆元,根据阿贝尔群的定义,-R 也一定在椭圆曲线上。
- 定义 P+Q = -R,也就是说椭圆曲线上任意两点的和也在椭圆曲线上,同样可以引申出椭圆曲线上任意三点的和为 0 即 P+Q+R = 0。如图:
- 假如 P=Q,则作椭圆曲线在 P 点的切线,与曲线相交于 R,则 R = P+P = 2P
2. 椭圆曲线乘法
根据上面椭圆曲线的加法可以得出下列等式:
- P+P = 2P(过点 P 切线作一条直线)
- P+2P = 3P(过点 P 和 2P 作一条直线)
- P+3P = 4P(过点 P 和 3P 作一条直线)
假设 P 是椭圆曲线上的一个点,正整数 K 乘以 P 可以总结成公式为:(k-1) * P + P = k * P
如果把 k 看作是两个数相乘即 k = m * n,则可以得出满足以下性质(在椭圆曲线密钥交换中会用到):(m * n) * P = m * (n * P) = (n * m)p = n * (m*P)
四、椭圆曲线的难题
非对称加密之所以难破解,根本原理就是基于一个数学上的难题,像 RSA 加密就是基于大质数因子分解困难的特性来支撑的,椭圆曲线的难题则是:椭圆曲线上的离散对数问题。
看过 RSA 加密算法原理那篇文章的读者可能会记得它是基于取模运算,椭圆曲线也是一样的,满足下面公式的曲线,其中 p 是质数,x、y、a、b 都是小于 p 的非负整数:
y^2 = x^3 + ax + b (mod p) { (4a^3 + 27b^2!)=0 }
来看一下 y^2 = x^3 - x 这个公式取模后的的图像(p=71):
可以看出,虽然很散乱,但是仔细看这些点都是关于一条直线对称的,这条直线就是 y=71/2 这条水平直线,并且原来椭圆曲线上定义的加法和乘法都可用。
假如选择一个点 P(4,8) 为基点,按照椭圆曲线的加法去运算 2P、3P… 这样的话,最后得到一个 k 次加法后的结果 kP(44,44),请问 k 是多少?
这时看一下上面的散点图,找到 (4,8) 和(44,44)这两个点,很难找出来通过几次椭圆曲线加法转变过去的,更何况这个是在公式中取模的那个质数等于 71 的情况下,如果把这个质数取得很大,难度就更大了,比特币中使用的 Secp256k1 这条曲线中取模的质数 p 等于:
p = 2^256 - 2^32 - 2^9 - 2^8 - 2^7 - 2^6 - 2^4 - 1
这样一个数,要逐一算出可能性取匹配几乎是不可能的。
总结一下椭圆曲线的数学依据:
K = kG
- G 为椭圆曲线上的一个点,叫基点;
- k 为正整数;
- 如果给定小 k 和 G,通过椭圆曲线加法法则去计算 K 很容易;
- 如果给定 K 和 G,求小 k 就非常困难。
一般规定大 K 为公开密钥,小 k 为私钥。
五、椭圆曲线的加密强度
这里我们那攻击分组对称加密 AES 算法的难度来做对比,如下表所示:
AES RSA ECC 80 1024 163 112 2240 233 128 3072 283
这组数据是国际期刊和一些学术论文中公认的结果,具体的实现步骤这里就不介绍,重点是要明白 ECC 的强度高,像 AES-128 位的密码强度和 RSA 的 3072 位密码强度大约相同,同样和 ECC 的 283 位密码强度相同,密码强度其实就是攻击的难度,也就是说攻击 128 位密钥的 AES 的算法的难度和 ECC283 位密钥的难度相当。
从图表中可以观察到 RSA 的密钥长度增长的很多,ECC 的密钥长度增长并不大,考虑到实际使用过程中的性能和未来能持续的安全行,ECC 是更好的选择。
六、Go语言使用椭圆曲线签名认证实现
package main
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"math/big"
"fmt"
)
//通过椭圆曲线完成签名和验证
func main() {
//声明明文
message := []byte("hello world")
//生成私钥
privateKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
//生成公钥
pub := privateKey.PublicKey
//将明文散列
digest := sha256.Sum256(message)
//签名
r, s, _ := ecdsa.Sign(rand.Reader, privateKey, digest[:])
//设置私钥的参数类型为曲线类型
param := privateKey.Curve.Params()
//获得私钥byte长度
curveOrderByteSize := param.P.BitLen() / 8
//获得签名返回值的字节
rByte, sByte := r.Bytes(), s.Bytes()
//创建数组
signature := make([]byte, curveOrderByteSize*2)
//通过数组保存了签名结果的返回值
copy(signature[curveOrderByteSize-len(rByte):], rByte)
copy(signature[curveOrderByteSize*2-len(sByte):], sByte)
//认证
//将明文做hash散列,为了验证的内容对比
digest = sha256.Sum256(message)
curveOrderByteSize = pub.Curve.Params().P.BitLen() / 8
//创建两个整形对象
r, s = new(big.Int), new(big.Int)
//设置证书值
r.SetBytes(signature[:curveOrderByteSize])
s.SetBytes(signature[curveOrderByteSize:])