ECC椭圆曲线加密算法:ECDH 和 ECDSA

ECC椭圆曲线加密算法:ECDH 和 ECDSA

Hi all,这里是整个椭圆曲线系列的第三部分。原文链接如下:

Elliptic Curve Cryptography: ECDH and ECDSAandrea.corbellini.name图标

想全面了解椭圆曲线的朋友可以先看看前两个部分,翻译得很棒:

Avery:ECC椭圆曲线加密算法:介绍zhuanlan.zhihu.com图标Avery:ECC椭圆曲线加密算法:有限域和离散对数zhuanlan.zhihu.com图标


在之前的文章中,我们已经认识了什么是椭圆曲线,并且为了更好得使用数学方法来处理椭圆曲线上的点,我们定义了「群」,接着我们又进一步将椭圆曲线限制在了整数取模素数的有限域上,椭圆曲线上的点在有限域上形成了循环子群,并且我们也介绍了「基点」「阶」「辅因子」的概念。

最后,我们知道在有限域上计算标量积是一个容易的过程,但是离散对数问题却是非常难的,现在我们就来看看这些理论是如何应用在密码学上的。

主要参数

椭圆曲线算法将会运用在有限域上的椭圆曲线所形成的循环子群上,因此,我们的算法需要以下几个参数:

  • 素数 p,用于确定有限域的范围
  • 椭圆曲线方程中的 ab 参数
  • 用于生成子群的的基点 G
  • 子群的阶 n
  • 子群的辅助因子 h

所以,我们算法的主要参数可以定义为一个六元组 (p, a, b, G, n, h)

随机曲线

「离散对数问题很困难」这种说法其实不完全正确,有一类椭圆曲线特别的弱以至于一些不怀好意的算法可以有效率的求解离散对数问题。例如,具有 p = hn(这意味着有限域的阶等于椭圆曲线的阶) 性质的所有曲线对于 smart 攻击是脆弱的,这就可以被用来在经典计算机上,多项式时间内解决离散对数问题。

现在,假设我给你一个曲线的主要参数,有可能我发现了一种新的没人知道的弱曲线,而且我已经在我给你的曲线上构建了一个快速算法,可以用来求解离散对数问题,我怎么样能让你确认我给你的曲线是安全的(换句话说,它不能被我用来做一些特殊攻击)?

为了解决这个问题,有时候我们需要另一个参数:种子 S,这是一个用来生成参数 a, b 或者基点 G,或者三个参数都生成的随机数,这些参数是通过计算种子 S 的哈希值得到的。哈希值,我们知道的,是正向计算容易,反向计算困难的。

种子是怎样生成一个随机曲线的:随机数的哈希值被用于计算曲线的不同参数
如果想要通过主要参数推导出种子 S 的值,就需要解决一个困难问题:逆向哈希

通过种子生成的曲线是可验证随机性的,使用哈希来生成参数的原则是众所周知的「Nothing-up-my-sleeve number」,这个原则也被运用在密码学中。

种子 S 可以提供一种保证,使提供曲线的人不会知道一些特殊的攻击漏洞。如果我将种子 S 和曲线一起提供给你,这就意味着我不会任意地选择参数 a 和 b,你也就可以相对确认我不能够发起一些特数目的的攻击,为什么是「相对」的呢,这个稍后解释。

生成和检查随机曲线的标准算法在 ANSI X9.62 中有描述,这是一个基于 SHA-1 的算法。如果你感兴趣,可以了解用于在 SECG 规范上生成可验证随机曲线的算法(找到 "Verifiably Random Curves and Base Point Generators")

我写了一个 python 脚本来验证当前所有随 OpenSSL 提供的随机曲线,强烈建议大家看看!

椭圆曲线密码学

花了好长的时间终于到了关键部分,我们简单来描述:

  1. 私钥是一个范围在 \{1, \dots, n - 1\} 中的随机整数 d,其中 n 是子群的阶
  2. 公钥是点 H,H = dG,其中 G 是子群的基点

你看,如果我们知道了 d 和 G(还有主要参数中的其他参数),求得 H 是很容易的。但是如果我们知道 H 和 G,想要求得私钥 d 很困难,因为这要求我们解决离散对数问题。

接下来我们讨论两种基于椭圆曲线密码学的两种公钥算法:用于加密的 ECDH(Elliptic curve Diffie-Hellman)和用于数字签名的 ECDSA(Elliptic curve Diffie-Hellman)

ECDH

ECDH 是椭圆曲线的笛福赫尔曼算法的变种,它其实不单单是一种加密算法,而是一种密钥协商协议,也就是说 ECDH 定义了(在某种程度上)密钥怎么样在通信双方之间生成和交换,至于使用这些密钥怎么样来进行加密完全取决通信双方。

我们需要解决的问题通常是这样的:Alice 和 Bob 想要安全通信,中间人可能会窃听消息,但是没办法解密消息

那么 ECDH 是这样的:

  1. Alice 和 Bob 生成各自的私钥和公钥,Alice 的私钥为 d_A ,公钥为 H_A = d_AG 。Bob 的私钥为 d_B ,公钥为 H_B = d_BG ,注意,Alice 和 Bob 需要使用一样的主要参数:在同一条曲线的同一个有限域上选择一样的基点 G。
  2. Alice 和 Bob 通过不安全信道交换各自的公钥 H_AH_B ,中间人可以窃听到 H_AH_B ,但是在无法攻破离散对数难题的情况下无法得到 d_Ad_B
  3. Alice 计算 S = d_A H_B (使用自身的私钥和 Bob 的公钥),Bob 计算 S = d_B H_A (使用自身的私钥和 Alice 的公钥),双方求得的 S 是一样的,因为 S = d_A H_B = d_A (d_B G) = d_B (d_A G) = d_B H_A

中间人只知道 H_AH_B 以及椭圆的公共参数,是无法算出共享密钥 S 的,这其实就是笛福赫尔曼问题:

给定三个点 P,aP,bP,那么 abP 的结果是什么?

或者我们可以这么理解:

给定三个整数 kk^xk^y ,那么 k^{xy} 的结果是什么?

(这种形式被用在了最原始的基于模运算的笛福赫尔曼算法上)

笛福赫尔曼密钥交换:Alice 和 Bob 可以很容易的计算出共享密钥,中间人就必须解决数学困难问题才能求解

笛福赫尔曼原理的介绍可以看这个视频,视频里解释了基于模运算的笛福赫尔曼算法(不是椭圆曲线)。

虽然没有数学证明直接说明椭圆曲线上的笛福赫尔曼问题是困难的,但是这个问题已经被公认为是个困难问题,因为人们相信这个问题的困难性是基于离散对数问题的困难性。但是可以肯定的是,这个问题的难度就到此为止不会更难了,因为只要解决了离散对数问题,笛福赫尔曼问题也就解决了。

现在 Alice 和 Bob 已经获得共享密钥,他们可以使用对称加密算法进行通信了。

举个栗子,他们可以使用 S 的 x 轴坐标作为 AES 或者 3DES 的密钥来加密信息,这多少有点像是 TLS 的操作,不同点是 TLS 将 x 轴坐标和网络连接相关的其他参数串联起来,然后计算这个串的哈希值。

实践 ECDH

我写了另一个 python 脚本生成基于椭圆曲线的公私钥和共享密钥。

不同于我们之前看到的例子,这个脚本用的是标准化的椭圆曲线,而不是在一个小的域内的简单曲线。我选择的曲线是 secp256k1,由 SECG 发表,这是比特币用来做数字签名的曲线,下面是曲线的主要参数:

  • p = 0xffffffff ffffffff ffffffff ffffffff ffffffff ffffffff fffffffe fffffc2f
  • a = 0
  • b = 7
  • x_G = 0x79be667e f9dcbbac 55a06295 ce870b07 029bfcdb 2dce28d9 59f2815b 16f81798
  • y_G = 0x483ada77 26a3c465 5da4fbfc 0e1108a8 fd17b448 a6855419 9c47d08f fb10d4b8
  • n = 0xffffffff ffffffff ffffffff fffffffe baaedce6 af48a03b bfd25e8c d0364141
  • h = 1

你也可以使用其他的曲线和主要参数,但需要保证素数域和曲线的正确性,否则脚本将不能运行

这个脚本特别简单,它包含了一些到目前位置我们讨论到的算法:点加、点乘、ECDH,建议大家看看然后跑一跑脚本,它将产生类似这样的一个输出:

Curve: secp256k1
Alice's private key: 0xe32868331fa8ef0138de0de85478346aec5e3912b6029ae71691c384237a3eeb

Alice's public key: (0x86b1aa5120f079594348c67647679e7ac4c365b2c01330db782b0ba611c1d677, 0x5f4376a23eed633657a90f385ba21068ed7e29859a7fab09e953cc5b3e89beba)

Bob's private key: 0xcef147652aa90162e1fff9cf07f2605ea05529ca215a04350a98ecc24aa34342 Bob's

public key: (0x4034127647bb7fdab7f1526c7d10be8b28174e2bba35b06ffd8a26fc2c20134a, 0x9e773199edc1ea792b150270ea3317689286c9fe239dd5b9c5cfd9e81b4b632)

Shared secret: (0x3e2ffbc3aa8a2836c1689e55cd169ba638b58a3a18803fcf7de153525b28c3cd, 0x43ca148c92af58ebdb525542488a4fe6397809200fe8c61b41a105449507083)

ECDHE

你可能听过 ECDHE 而没听过 ECDH,ECHDE 中的 E 代表着「短暂的」,是指交换的密钥是暂时的动态的,而不是固定的静态的。

举个栗子,在 TLS 中就使用了 ECDHE,连接建立时,服务器和客户端都动态生成公私钥,这些密钥在之后会用于 TLS 认证和通信双方之间的信息交换。

使用 ECDSA 签名

假设这样一个场景:Alice 想要使用她的私钥 d_A 来签名,Bob 想用 Alice 的公钥 H_A 要验证签名,只有 Alice 才能提供正确的签名,而每个人都可以验证签名。

ECDSA 是 DSA 作用于椭圆曲线的一个变种算法。Alice 和 Bob 仍然使用同样的曲线,ECDSA 需要使用明文的哈希结果,而不是明文本身。哈希函数的选择取决于使用者,但是需要明确的是必须选择加密安全的哈希函数,为了使哈希结果的比特长度和 n (子群的阶)的比特长度一致,消息的哈希结果需要被截断,被截断后的哈希值会是一个整数,我们用 z 来表示。

Alice 使用算法来签名的步骤如下:

  1. \{1,…,n−1\} 范围内选取一个随机数 kn 是子群的阶)
  2. 计算点 P = kGG 是子群的基点)
  3. 计算数字  r = x_p\bmod{n}x_ppx 轴坐标)
  4. 如果 r = 0 ,另选一个 k 并重新计算
  5. 计算 s = k^{-1} (z + rd_A)\bmod{n}d_A 是 Alice 的私钥, k^{-1}k\bmod{n} 的乘法逆元)
  6. 如果 s = 0 ,另选一个 k 并重新计算

(r, s) 就是签名。

Alice 用自己的私钥和随机数 k 签名了哈希值 z。Bob 用 Alice 的公钥来验证签名的正确性

通俗的说,这个算法一开始生成了 k,得益于点乘(这是一个数学困难问题)k 被隐藏在了 r 中,然后通过等式 s = k^{-1} (z + rd_A)\bmod{n} 将 r 绑定到了消息散列值上。

为了计算 s,我们必须计算 k 的逆 mod n,在之前的文章中说过只有在 n 是素数的情况下才能保证这一过程,如果子群的阶不是一个素数,ECDSA 将不起作用。几乎所有标准的曲线都是素数阶的,这肯定不是巧合,非素数阶的那些曲线是不能被 ECDSA 使用的。

验证签名

为了验证签名,我们需要 Alice 的公钥 H_A ,被截断的哈希值 z,还有签名 (r, s)

  1. 计算整数 u_1 = s^{-1} z \bmod{n}
  2. 计算整数 u_2 = s^{-1} r \bmod{n}
  3. 计算点 P = u_1 G + u_2 H_A

只有当 r = x_P \bmod{n} 的时候,签名才被成功验证

算法的正确性

算法的逻辑一开始看不是很容易理解,如果我们把前面用到的公式整合联立一下,就变得清晰了

我们从 P = u_1 G + u_2 H_A 开始,通过公钥的定义我们知道 H_A = d_A Gd_A 是私钥),所以我们得到:

\begin{array}{rl} P & = u_1 G + u_2 H_A \\ & = u_1 G + u_2 d_A G \\ & = (u_1 + u_2 d_A) G \end{array}

使用 u_1u_2 的定义,可以得到:

\begin{array}{rl} P & = (u_1 + u_2 d_A) G \\ & = (s^{-1} z + s^{-1} r d_A) G \\ & = s^{-1} (z + r d_A) G \end{array}

这里为了简单先忽略 \text{mod}\ n ,因为由 G 生成的循环子群的阶为 n ,所以这里的 \text{mod}\ n 其实也是没必要的。

再往前,我们定义了 s = k^{-1} (z + rd_A) \bmod{n} ,式子两边同乘以 k 再同除 s ,也就是: k = s^{-1} (z + rd_A) \bmod{n} ,把这个结果带到上面关于 P 的等式中得到:

\begin{array}{rl}   P & = s^{-1} (z + r d_A) G \\   & = k G \end{array}

这不就是我们在签名时候的第二个步骤得到的等式吗!在生成签名和验证签名的时候,我们使用了不同的等式计算了同样的点 P,这就是这个算法能够使用的原因。

实践 ECDSA

我仍然写了个生成和验证签名的 python 脚本,这个脚本中的代码和之前 ECDH 中的代码有一部分相同,特别是主要参数和公私钥对生成算法。

脚本产生类似这样的一个输出:

Curve: secp256k1
Private key: 0x9f4c9eb899bd86e0e83ecca659602a15b2edb648e2ae4ee4a256b17bb29a1a1e

Public key: (0xabd9791437093d377ca25ea974ddc099eafa3d97c7250d2ea32af6a1556f92a, 0x3fe60f6150b6d87ae8d64b78199b13f26977407c801f233288c97ddc4acca326)

Message: b'Hello!'
Signature: (0xddcb8b5abfe46902f2ac54ab9cd5cf205e359c03fdf66ead1130826f79d45478, 0x551a5b2cd8465db43254df998ba577cb28e1ee73c5530430395e4fba96610151)
Verification: signature matches

Message: b'Hi there!'
Verification: invalid signature

Message: b'Hello!'
Public key: (0xc40572bb38dec72b82b3efb1efc8552588b8774149a32e546fb703021cf3b78a, 0x8c6e5c5a9c1ea4cad778072fe955ed1c6a2a92f516f02cab57e0ba7d0765f8bb)
Verification: invalid signature

脚本首先签名了一段消息(字节串 “Hello”),接着验证签名。然后,脚本接着尝试基于其他消息(”Hi there!”)去验证同样的签名,然后验证失败了。再接着,脚本尝试用正确的消息去验证签名,但是验证的过程使用了另一个随机的公钥,然后验证失败了。

k 的重要性

在生成 ECDSA 签名的过程中,保证 k 的绝对私密非常重要。如果所有的签名都使用一样的 k,或者使用的随机数生成器不够随机(可预测),那么攻击者就能够找出私钥!

索尼在几年前就犯过这样的错误,正常来说, PlayStation 3 只能运行被索尼的 ECDSA 算法签名过的游戏,如果我想创建一个 PS3 的新游戏,我并不能在没有索尼签名的情况下向市场推广我的游戏。问题来了,索尼在 PS3 中的所有签名都是用固定的 k 生成的。

很明显,索尼的随机数生成器的灵感来自于 XKCD 或 Dilbert
我还是把这两张可爱的图贴上来了

在这种情况下,我们就可以很容易反推出索尼的私钥 d_ S ,只需要买两个游戏,取出它们的哈希值 (z_1,  z_2) 、签名((r_1, s_1) (r_2, s_2)),当然还有椭圆曲线的公共参数,接着:

  • 注意 r_1 = r_2 (因为 r = x_P \bmod{n} ,而 P = kG ,因为 k 相等,所以 r 也相等)
  • 想想看(s_1 - s_2) \bmod{n} = k^{-1} (z_1 - z_2) \bmod{n} (这个结果直接来自于 s 的等式)
  • 等式两边同乘 kk (s_1 - s_2) \bmod{n} = (z_1 - z_2) \bmod{n}
  • (s_1 - s_2) 得到  k = (z_1 - z_2)(s_1 - s_2)^{-1} \bmod{n}

最后得到的这个等式使我们仅仅需要两个哈希和对应的签名,我们就可以得到 k,现在我们可以通过 s 的等式得到私钥了:

s = k^{-1}(z + rd_S) \bmod{n}\ \ \Rightarrow\ \ d_S = r^{-1} (sk - z) \bmod{n}

类似的方法在 k 可预测的情况下也是能够使用的。

周末愉快

椭圆曲线系列的最后一篇文章,会介绍一些涉及离散对数算法的技术、椭圆曲线密码学现存的问题以及其与 RSA 的比较。

(ps. 如果翻译得不够准确,欢迎指出和探讨;如果读下来能有些收获,告诉我!我会很高兴的!)

编辑于 2019-06-12