区块链中密钥对的生成原理

bitcoin中的密钥综述

关于bitcon中使用的椭圆曲线加密体制的一些事实:

  • 私钥长度 32bytes
  • 公钥长度 64bytes (未压缩形式) 或者 32bytes(压缩形式)+ 1byte(前缀)
  • 椭圆曲线Csecp256k1曲线
  • 椭圆曲线加密体制基于模运算

在本文中,我们唯一的输入就是私钥。公钥可以唯一地从私钥推导而来。我们先使用openssl命令行生成一个密钥对样例,然后再尝试编写C语言代码进行同样的操作。

在OpenSSL命令行中生成

私钥

一个私钥是一个随机选取的32 bytes的数字,并且我们都知道32 bytes存储的数字可以转换成为一个非常大的数值,最大可以达到2256。去猜测这样一个数字是很荒谬的,因此可以认为它的生成具有很高的随机性。

得到一个全新的私钥是非常容易的:

$ openssl ecparam -name secp256k1 -genkey -out ec-priv.pem

生成的结果文件ec-prive.pem中包含了曲线名称(secp256k1)和私钥,它们连同一些其它字符一起被base64编码。

这个文件可以被快速解码为可读的16进制形式:

openssl ec -in ec-priv.pem -text -noout

以下是我所生成的密钥对形式(你生成的与我的不相同):

read EC key
Private-Key: (256 bit)
priv:
    16:26:07:83:e4:0b:16:73:16:73:62:2a:c8:a5:b0:
    45:fc:3e:a4:af:70:f7:27:f3:f9:e9:2b:dd:3a:1d:
    dc:42
pub: 
    04:82:00:6e:93:98:a6:98:6e:da:61:fe:91:67:4c:
    3a:10:8c:39:94:75:bf:1e:73:8f:19:df:c2:db:11:
    db:1d:28:13:0c:6b:3b:28:ae:f9:a9:c7:e7:14:3d:
    ac:6c:f1:2c:09:b8:44:4d:b6:16:79:ab:b1:d8:6f:
    85:c0:38:a5:8c
ASN1 OID: secp256k1

其中私钥被显示为这样的形式会更直观:

16 26 07 83 e4 0b 16 73
16 73 62 2a c8 a5 b0 45
fc 3e a4 af 70 f7 27 f3
f9 e9 2b dd 3a 1d dc 42

这个私钥代表着你的身份,必须安全地将它保存。也就是说,如果这不是我用来举例的而生成的私钥,我不会将它分享给任何人。我们会用这个私钥来签名我们的信息,这样的话所有人就会相信这个信息确实是我本人发出的。如果其他人窃取了你的私钥,他将能够伪造你的身份。在bitcoin中,他就能够拿走你所有的钱。一定要当心!

公钥

默认情况下,一个公钥是由两个 32bytes 的大数组成,这种就是所谓的未压缩形式。这两个数字代表着二维坐标系上,secp256k1椭圆曲线上的一个点(x,y),它是符合以下方程的:

y2 = x3 + 7

这个点的坐标是由私钥决定的,但是反之,从点的坐标推断私钥是不可行的。毕竟,这就是椭圆曲线加密算法安全性的保障。根据依赖特性,结合上述方程和一个x值,可以计算得到一个对应的y值。事实上,压缩形式的公钥就是根据这个原理将y值省略掉来节省空间的。

我们可以将上述密钥对中的公钥部分取出,存储到一个叫做ec-pub.pem的外部文件中:

$ openssl ec -in ec-priv.pem -pubout -out ec-pub.pem

接着将它解码:

$ openssl ec -in ec-pub.pem -pubin -text -noout

未包含私钥部分的文本形式就会显示出来:

read EC key
Private-Key: (256 bit)
pub: 
    04:82:00:6e:93:98:a6:98:6e:da:61:fe:91:67:4c:
    3a:10:8c:39:94:75:bf:1e:73:8f:19:df:c2:db:11:
    db:1d:28:13:0c:6b:3b:28:ae:f9:a9:c7:e7:14:3d:
    ac:6c:f1:2c:09:b8:44:4d:b6:16:79:ab:b1:d8:6f:
    85:c0:38:a5:8c
ASN1 OID: secp256k1

一个更加直观的版本:

04

82 00 6e 93 98 a6 98 6e
da 61 fe 91 67 4c 3a 10
8c 39 94 75 bf 1e 73 8f
19 df c2 db 11 db 1d 28

13 0c 6b 3b 28 ae f9 a9
c7 e7 14 3d ac 6c f1 2c
09 b8 44 4d b6 16 79 ab
b1 d8 6f 85 c0 38 a5 8c

这个未压缩的版本包含了65个字节:

  • 一个不变的04前缀
  • 32 bytes 的 x 坐标
  • 32 bytes 的 y 坐标

很容易就可以将它转换为压缩形式。我们只需省略掉y并改掉它的前缀。这个新前缀是根据y来决定的:前缀是 02表示y是偶数值,前缀是 03表示y是奇数值。

通过:

$ openssl ec -in ec-pub.pem -pubin -text -noout -conv_form compressed

命令行中显示:

read EC key
Private-Key: (256 bit)
pub: 
    02:82:00:6e:93:98:a6:98:6e:da:61:fe:91:67:4c:
    3a:10:8c:39:94:75:bf:1e:73:8f:19:df:c2:db:11:
    db:1d:28
ASN1 OID: secp256k1

结论, 压缩形式占据33bytes:

  • 一个0203 的前缀
  • 32bytes 的 x 坐标

从代码中生成

(注:原文作者的 repository)

密钥对的产生过程是冗长的,然而使用OpenSSL来完成却不难。我ec.h中声明了一个帮助函数,原型如下:

EC_KEY *bbp_ec_new_keypair(const uint8_t *priv_bytes);

我们来一起分析一下原文中的部分代码,一些OpenSSL中的数据结构如下:

  • BN_CTX, BIGNUM
  • EC_KEY
  • EC_GROUP, EC_POINT

前两个结构体属于OpenSSL的任意精度算数(大数)部分,因为我们需要处理非常大的数字。EC_KEY可以是一个完整的密钥对或者是一个单独的公钥。EC_GROUPEC_POINT帮助我们根据私钥来计算公钥。

最重要的部分,我们初始化了一个EC_KEY结构来存储一对密钥对:

key = EC_KEY_new_by_curve_name(NID_secp256k1);

填充私钥是很容易的,但是需要一个过渡过程。再将要输入的priv_bytes填入密钥对之前,我们需要将它转换为BIGNUM,在这里命名为priv

BN_init(&priv);
BN_bin2bn(priv_bytes, 32, &priv);
EC_KEY_set_private_key(key, &priv);

对于复杂的大数操作,OpenSSL需要一个上下文环境,这就是BN_CTX需要被创建的原因。公钥的推导需要一个深层次的数学理解,而这并不是这篇文章所要达到的目标。简单来说,就是我们在曲线上定位一个固定的点G(generator, 代码中的group), 然后乘以标量私钥n,而这是一个在模运算中不可逆的过程。它的结果P = n * G就是第二个点,公钥pub。最终,公钥被载入到密钥对中:

ctx = BN_CTX_new();
BN_CTX_start(ctx);

group = EC_KEY_get0_group(key);
pub = EC_POINT_new(group);
EC_POINT_mul(group, pub, &priv, NULL, NULL, ctx);
EC_KEY_set_public_key(key, pub);

最后时刻,我们要在ex-ec-keypair.c测试密钥对。我们期待如果给定一个固定的私钥,通过代码产生的结果和命令行中产生的结果相同。

    原文作者:marmalade
    原文地址: https://segmentfault.com/a/1190000013176420
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞