比特币开发系列 - 椭圆曲线密钥

比特币开发系列 - 椭圆曲线密钥

上一篇文章中,我向你概述了公钥密码学及其与区块链的关系。你将开始熟悉用于生成比特币密钥对的真实加密函数。

希望通过尽可能清晰的方式来讨论这个主题,这篇文章可能会显得有点啰嗦。无论如何,你的耐心将会得到回报,因为从这里开始,课程将变得轻松。

比特币密钥概述

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

  • 私钥长度为 32 字节。
  • 公钥长度为 64 字节(未压缩形式)或 32 字节(压缩形式),再加上 1 字节前缀。
  • 椭圆曲线 Csecp256k1 曲线。
  • 椭圆曲线密码学基于 模算术

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

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

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

私钥

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

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

    $ 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)$$ 坐标,该曲线具有以下公式:

$$y^2 = x^3 + 7$$

点的位置由私钥确定,但从点坐标推断私钥是不可行的。毕竟,这就是使得椭圆曲线密码学安全的原因。由于其依赖性质,$$y$$ 可以从 $x$ 和曲线公式推导出来。事实上,压缩 形式通过省略 $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 字节的 $x$ 坐标。
  • 32 字节的 $y$ 坐标。

将未压缩的公钥转换为压缩形式很容易,我们只需省略 $y$ 并根据其值更改前缀。对于 $y$ 的偶数值,第一个字节变为 02,对于奇数值,变为 03。我的 $y$ 以 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 字节:

  • 常量 0203 前缀。
  • 32 字节的 $x$ 坐标。

从代码生成密钥对

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

    EC_KEY *bbp_ec_new_keypair(const uint8_t *priv_bytes);

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

  • BN_CTXBIGNUM
  • EC_KEY
  • EC_GROUPEC_POINT

前两个 struct 属于 OpenSSL 的任意精度算术领域,因为我们需要处理非常大的数字。所有其他类型都与 EC 加密有关。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。公钥派生需要更深入地理解 EC 数学,这不是本系列的目的。基本上,我们在曲线上定位一个固定点 $G$(生成器,代码中的 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 中测试我们的密钥对了。我们期望代码输出与我们在命令行中使用 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_ECPublicKeyNULL 参数调用来获取实际长度,以便为 pub 分配足够的字节来容纳输出。最终,i2o_ECPublicKey 将公钥从内部表示转换为八位字节,因此有 i2o 前缀(octet = byte)。编码字节通过 pub_copy 写入 pub

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

获取代码

GitHub 上查看完整源代码。

下一篇

你已经学会如何生成新的 EC 密钥对。通过一些自定义代码,你还学会了如何从原始字节创建密钥对。

下一篇文章中,你将使用 EC 密钥对来生成和验证 数字签名。如果你喜欢这篇文章,请分享。

我是 AI 翻译官,为大家转译优秀英文文章,如有翻译不通的地方,在这里修改,还请包涵~

点赞 0
收藏 1
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Davide De Rosa
Davide De Rosa
江湖只有他的大名,没有他的介绍。