椭圆曲线数字签名算法

https://aaron67.cc/2020/09/30/ecdsa/

在文章非对称加密和签名认证中,我们介绍了双钥系统的两种应用场景:

  • 加密解密时,公钥用于加密,私钥用于解密
  • 身份认证时,私钥用于签名,公钥用于验证

椭圆曲线密码学(ECC,Elliptic Curve Cryptogphay)是一种流行的非对称加密算法,其背后的数学原理,是椭圆曲线上的离散对数难题。我们还知道,ECC 的私钥,本质是一个整数,其对应的公钥,是椭圆曲线上的一个点。

在将 ECC 作为双钥系统使用时,针对不同的应用场景,会涉及到不同的算法。常见的有

  • 在加密和解密时使用的椭圆曲线集成加密框架(ECIES,Elliptic Curve Integrated Encryption Schema)
  • 用于协商和交换公共密钥的椭圆曲线 Diffie-Hellman 密钥交换算法(ECDH,Elliptic Curve Diffie-Hellman Key Exchange)
  • 用于生成和验证数字签名的椭圆曲线数字签名算法(ECDSA,Elliptic Curve Digital Signature Algorithm)

本文将介绍并实现 ECDSA 的相关内容。

签名

用私钥 a a a 对消息 m m m 签名,得到的结果是两个整数 ( r , s ) (r, s) (r,s) ,计算过程如下。

  • 随机生成临时私钥 k k k,并计算其对应的公钥 K = k ⋅ G = ( x K , y K ) K = k \cdot G = (x_K, y_K) K=kG=(xK,yK)
  • 计算 r = x K   m o d   n r = x_K \bmod{n} r=xKmodn,若 r r r 为 0,则回到第一步
  • 计算消息 m m m 的哈希 e = h a s h ( m ) e = hash(m) e=hash(m),并将 e e e 的二进制序列转成一个整数
  • 计算 s = k − 1 ( e + r a )   m o d   n s = k^{-1}(e + ra) \bmod{n} s=k1(e+ra)modn,若 s s s 为 0,则回到第一步
  • 得到签名 ( r , s ) (r, s) (r,s)
import hashlib


def sha256(payload: bytes) -> bytes:
    return hashlib.sha256(payload).digest()


def double_sha256(payload: bytes) -> bytes:
    return sha256(sha256(payload))
import random


def hash_to_int(message: bytes) -> int:
    """Calculate the bitcoin double-sha256 hash of the message, return as an integer"""
    h = double_sha256(message)
    return int.from_bytes(h, byteorder='big')


def sign(private_key: int, message: bytes) -> tuple:
    """Create ECDSA signature (r, s)"""
    e = hash_to_int(message)
    r, s = 0, 0
    while not r or not s:
        k = random.randrange(1, curve.n)
        k_x, _ = scalar_multiply(k, curve.g)
        r = k_x % curve.n
        s = ((e + r * private_key) * modular_multiplicative_inverse(k, curve.n)) % curve.n
    return r, s

如果每次签名都使用相同的 k k k,当知道了消息 e 1 e_1 e1 e 2 e_2 e2 和签名 ( r 1 , s 1 ) (r_1, s_1) (r1,s1) ( r 2 , s 2 ) (r_2, s_2) (r2,s2) 时,有

  • r 1 = r 2 r_1 = r_2 r1=r2,因为 k k k 相同
  • 根据 s s s 的定义,有 s 1 − s 2 = k − 1 ( e 1 − e 2 )   m o d   n s_1 - s_2 = k^{-1}(e_1 - e_2) \bmod{n} s1s2=k1(e1e2)modn
  • 将上式两边同时乘以 k k k 再除以 ( s 1 − s 2 ) (s_1 - s_2) (s1s2),有 k = ( s 1 − s 2 ) − 1 ( e 1 − e 2 )   m o d   n k = (s_1 - s_2)^{-1}(e_1 - e_2) \bmod{n} k=(s1s2)1(e1e2)modn

这样就得到了 k k k,再根据 s = k − 1 ( e + r a )   m o d   n s = k^{-1}(e + ra) \bmod{n} s=k1(e+ra)modn,从而反推出 a = r − 1 ( s k − e )   m o d   n a = r^{-1}(sk - e) \bmod{n} a=r1(ske)modn。类似的攻击方法在 k k k 值可预测时也能使用。

所以请务必注意,每次签名时使用的 k k k,都需要保证绝对私密且生成时足够随机,以保证私钥 a a a 的安全。

验签

用公钥 A A A 和消息 m m m 验证签名 ( r , s ) (r, s) (r,s),过程如下。

  • 计算消息 m m m 的哈希 e = h a s h ( m ) e = hash(m) e=hash(m),并将 e e e 的二进制序列转成一个整数
  • 计算整数 u 1 = s − 1 e   m o d   n u_1 = s^{-1}e \bmod{n} u1=s1emodn
  • 计算整数 u 2 = s − 1 r   m o d   n u_2 = s^{-1}r \bmod{n} u2=s1rmodn
  • 计算点 P = u 1 ⋅ G + u 2 ⋅ A = ( x P , y P ) P = u_1 \cdot G + u_2 \cdot A = (x_P, y_P) P=u1G+u2A=(xP,yP)
  • 当且仅当 r = x P   m o d   n r = x_P \bmod{n} r=xPmodn 时,验签成功
def verify_signature(public_key: tuple, message: bytes, signature: tuple) -> bool:
    """Verify signature with public key and message"""
    e = hash_to_int(message)
    r, s = signature
    w = modular_multiplicative_inverse(s, curve.n)
    u1 = (w * e) % curve.n
    u2 = (w * r) % curve.n
    x, _ = add(scalar_multiply(u1, curve.g), scalar_multiply(u2, public_key))
    return r == (x % curve.n)

证明

在开始之前,需要先明确一点:椭圆曲线上点的加法运算,符合交换律和结合律。

所以有 a G + b G = ( a + b ) ⋅ G aG + bG = (a + b) \cdot G aG+bG=(a+b)G,因为

a G + b G = G + G + . . . + G ⏟ _ a   个 + G + G + . . . + G ⏟ _ b   个 = G + G + . . . + G ⏟ _ ( a + b )   个 = ( a + b ) ⋅ G aG + bG = \underbrace{G + G + ... + G}\_{a\ 个} + \underbrace{G + G + ... + G}\_{b\ 个} = \underbrace{G + G + ... + G}\_{(a + b)\ 个} = (a + b) \cdot G aG+bG= G+G+...+G_a + G+G+...+G_b = G+G+...+G_(a+b) =(a+b)G

进一步的,有 a ⋅ b G = a b ⋅ G a \cdot bG = ab \cdot G abG=abG,因为

a ⋅ b G = b G + b G + . . . + b G ⏟ _ a   个 = ( G + . . . + G ) + . . . + ( G + . . . + G ) ⏟ _ a   个 = G + G + . . . + G ⏟ _ a b   个 = a b ⋅ G a \cdot bG = \underbrace{bG + bG + ... + bG}\_{a\ 个} = \underbrace{(G + ... + G) + ... + (G + ... + G)}\_{a\ 个} = \underbrace{G + G + ... + G}\_{ab\ 个} = ab \cdot G abG= bG+bG+...+bG_a = (G+...+G)+...+(G+...+G)_a = G+G+...+G_ab =abG

让我们借助上面这两个推论,通过公式变换,证明验签过程的正确性。

已知 A = a G A = aG A=aG,则

P = u 1 ⋅ G + u 2 ⋅ A = u 1 ⋅ G + u 2 ⋅ a G = ( u 1 + u 2 a ) ⋅ G P = u_1 \cdot G + u_2 \cdot A = u_1 \cdot G + u_2 \cdot aG = (u_1 + u_2a) \cdot G P=u1G+u2A=u1G+u2aG=(u1+u2a)G

带入 u 1 u_1 u1 u 2 u_2 u2 的定义,则

P = ( u 1 + u 2 a ) ⋅ G = ( s − 1 e + s − 1 r a ) ⋅ G = s − 1 ( e + r a ) ⋅ G P = (u_1 + u_2a) \cdot G = (s^{-1}e + s^{-1}ra) \cdot G = s^{-1}(e + ra) \cdot G P=(u1+u2a)G=(s1e+s1ra)G=s1(e+ra)G

请注意,这里我们忽略了 u 1 u_1 u1 u 2 u_2 u2 定义中的模 n n n 运算,这不会有任何问题,因为 a ⋅ G = ( a   m o d   n ) ⋅ G a \cdot G = (a \bmod{n}) \cdot G aG=(amodn)G。如果你想知道为什么,可以搜索关键字“循环子群”阅读更多资料。

我们还知道 s = k − 1 ( e + r a )   m o d   n s = k^{-1}(e + ra) \bmod{n} s=k1(e+ra)modn,将等式两边同时乘以 k k k 再除以 s s s,就会得到

k = s − 1 ( e + r a )   m o d   n k = s^{-1}(e + ra) \bmod{n} k=s1(e+ra)modn

也就是说,如果 ( r , s ) (r, s) (r,s) 正确,验签时计算出的点 P P P 就是签名时临时私钥 k k k 对应的公钥。

P = s − 1 ( e + r a ) ⋅ G = k ⋅ G = ( x P , y P ) P = s^{-1}(e + ra) \cdot G = k \cdot G = (x_P, y_P) P=s1(e+ra)G=kG=(xP,yP)

根据 r r r 的定义,即当且仅当 r = x P   m o d   n r = x_P \bmod{n} r=xPmodn 时,验签成功。

验证

我们可以写一个简单的例子来测试代码。

if __name__ == '__main__':
    # 私钥
    priv_key = 0xf97c89aaacf0cd2e47ddbacc97dae1f88bec49106ac37716c451dcdd008a4b62
    # 公钥
    pub_key = scalar_multiply(priv_key, curve.g)
    # 要签名的消息
    plain_text = '你好世界'
    digest = sha256(plain_text.encode('utf-8'))
    # 签名
    sig_r, sig_s = sign(priv_key, digest)
    print(' r =', sig_r)
    print(' s =', sig_s)
    # 验证签名 (r, s)
    print(verify_signature(pub_key, digest, (sig_r, sig_s)))

运行结果为

r = 114587593887127314608220924841831336233967095853165151956820984900193959037698
s = 24000727837347392504013031837120627225728348681623127776947626422811445180558
True

输出符合预期。

签名的“对称性”

对 ECDSA 签名 ( r , s ) (r, s) (r,s),在验证时如果使用 ( r , − s   m o d   n ) (r, -s \bmod{n}) (r,smodn),则也能验签成功。

if __name__ == '__main__':
    #
    # ...
    #
    # 验证签名 (r, -s)
    negative_s = -sig_s % curve.n
    print('-s =', negative_s)
    print(verify_signature(pub_key, digest, (sig_r, negative_s)))

运行结果为

 r = 106466997694091524629965845090867478458136818253940782993316021692670806749258
 s = 62904381853960967395432938678025872957596216909471919761592805395928247608025
True
-s = 52887707383355228028138046330662034895241347369602984621012357745589913886312
True

这个“特性”本文暂不展开介绍,直接把结论放在这里,以保证整体内容的完整性。

完整代码

sign.py

参考

  • 11
    点赞
  • 43
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值