您现在的位置:kastop>> Kas信息 Kaspa网络>>正文内容

比特币密钥概述

关于比特币椭圆曲线密码学的一些事实:

  • 私钥长度为 32 字节。

  • 公钥长度为 64 字节(未压缩形式)或 32 字节(压缩形式),再加上 1 字节前缀。

  • 椭圆曲线 C 是 secp256k1 曲线。

  • 椭圆曲线密码学基于 模算术。

在这个复杂的背景下,我们的唯一输入是私钥。公钥是从私钥唯一派生出来的,无论是未压缩还是压缩形式。首先,我们将使用 OpenSSL 从命令行生成一个示例密钥对。接下来,我们将通过 C 代码执行相同的操作。

对于 MAC 用户:我强烈建议你在 Homebrew 版本的 OpenSSL 上测试此代码。OS X 10.10 预装的是已经过时的 0.9.8 版本,因此某些命令可能无法正常工作!

值得一提的是,比特币核心开发人员正逐渐从 OpenSSL 转向 他们自己的 secp256k1 加密实现。

私钥

私钥是随机选择的 32 字节数字,你知道 32 字节可以构成一个非常大的数字,有 22562256 那么大。因此,假设它是以非常高度的随机性生成的,那么这样的数字是极其难以猜测的。

获取一个新的随机密钥很简单:

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

输出文件 ec-priv.pem 包括曲线名称(secp256k1)和私钥,两者都经过 base64 编码以及其他附加内容。该文件可以快速解码为文本,以便你可以查看原始十六进制数:

    $ 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

密钥反映了我们的身份,因此我们希望将其保密且安全。我的意思是,如果这不是一个示例私钥,我是不会与任何人分享它的。我们将使用它对我们的消息进行签名,以便世界可以相信这些消息确实是我们写的。如果有人窃取了我们的私钥,他将能够伪造我们的身份。在比特币中,他将能够拿走我们的钱。小心!

公钥

默认情况下,公钥由两个 32 字节数字组成,即所谓的 未压缩 形式。这些数字代表 secp256k1 椭圆曲线上一个点的 (x,y)(x,y) 坐标,该曲线具有以下公式:

y2=x3+7y2=x3+7

点的位置由私钥确定,但从点坐标推断私钥是不可行的。毕竟,这就是使得椭圆曲线密码学安全的原因。由于其依赖性质,yy 可以从 xx 和曲线公式推导出来。事实上,压缩 形式通过省略 yy 值来节省空间。

从私钥中,可以提取出公共部分并将其存储到我称为 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 字节的 xx 坐标。

  • 32 字节的 yy 坐标。

将未压缩的公钥转换为压缩形式很容易,我们只需省略 yy 并根据其值更改前缀。对于 yy 的偶数值,第一个字节变为 02,对于奇数值,变为 03。我的 yy 以 8c 结尾,因此新前缀为 02:

    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

可以使用以下命令完成相同操作:

    $ 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

最后,压缩转换形式占用 33 字节:

  • 常量 02 或 03 前缀。

  • 32 字节的 xx 坐标。

从代码生成密钥对

密钥对生成任务繁琐,但在 OpenSSL 库的帮助下并不困难。我在 ec.h 中编写了一个辅助函数,声明如下:

    EC_KEY *bbp_ec_new_keypair(const uint8_t *priv_bytes);

让我们一起分析其中的部分代码。涉及到几个 OpenSSL 数据结构:

  • BN_CTX、BIGNUM

  • EC_KEY

  • EC_GROUP、EC_POINT

前两个 struct 属于 OpenSSL 的任意精度算术领域,因为我们需要处理非常大的数字。所有其他类型都与 EC 加密有关。EC_KEY 可以是完整的密钥对(私钥 + 公钥)或仅为公钥,而 EC_GROUP 和 EC_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。公钥派生需要更深入地理解 EC 数学,这不是本系列的目的。基本上,我们在曲线上定位一个固定点 GG(生成器,代码中的 group),并将其乘以标量私钥 nn,这是一个在模算术中几乎不可逆的操作。得到的 P=n∗GP=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 中测试我们的密钥对了。我们期望代码输出与我们在命令行中使用 openssl 得到的结果相同,假设 priv_bytes 包含我们的示例私钥:

    uint8_t priv_bytes[32] = {       0x16, 0x26, 0x07, 0x83, 0xe4, 0x0b, 0x16, 0x73,       0x16, 0x73, 0x62, 0x2a, 0xc8, 0xa5, 0xb0, 0x45,       0xfc, 0x3e, 0xa4, 0xaf, 0x70, 0xf7, 0x27, 0xf3,       0xf9, 0xe9, 0x2b, 0xdd, 0x3a, 0x1d, 0xdc, 0x42
   };
   
   EC_KEY *key;
   uint8_t priv[32];
   uint8_t *pub;
   const BIGNUM *priv_bn;
   
   point_conversion_form_t conv_forms[] = {
      POINT_CONVERSION_UNCOMPRESSED,
      POINT_CONVERSION_COMPRESSED
   };
   ...    
   /* 1 */
   
   key = bbp_ec_new_keypair(priv_bytes);
   ...    
   /* 2 */
   
   priv_bn = EC_KEY_get0_private_key(key);
   BN_bn2bin(priv_bn, priv);
   ...    
   /* 3 */
   
   for (i = 0; i < sizeof(conv_forms) / sizeof(point_conversion_form_t); ++i) {
       size_t pub_len;
       uint8_t *pub_copy;
   
       EC_KEY_set_conv_form(key, conv_forms[i]);
   
       pub_len = i2o_ECPublicKey(key, NULL);
       pub = calloc(pub_len, sizeof(uint8_t));    
       /* pub_copy is needed because i2o_ECPublicKey alters the input pointer */
       pub_copy = pub;        if (i2o_ECPublicKey(key, &pub_copy) != pub_len) {
           ...
       }
       ...
   }
   ...

测试步骤如下:

  1. 使用 priv_bytes 初始化一个 EC_KEY 密钥对。

  2. 通过 BIGNUM 将私钥重新放入 priv 中。

  3. 将派生的公钥以所有转换形式放入 pub 中。

第三步是最复杂的。首先设置转换形式,这将影响公钥的长度(33 或 65)。通过对 i2o_ECPublicKey 的 NULL 参数调用来获取实际长度,以便为 pub 分配足够的字节来容纳输出。最终,i2o_ECPublicKey 将公钥从内部表示转换为八位字节,因此有 i2o 前缀(octet = byte)。编码字节通过 pub_copy 写入 pub。

尝试运行程序并将其与 openssl 命令行工具的输出进行比较。

获取代码

在 GitHub 上查看完整源代码。



感动 同情 无聊 愤怒 搞笑 难过 高兴 路过
【字体:小 大】【收藏】【打印文章】 【 打赏 】 【查看评论】

相关文章

    没有相关内容