以下为翻译文章,英文原版链接在这里,文章中的“我”指的是原文作者

通过代码生成密钥对

密钥对的生成工序比较繁琐,然而通过OpenSSL这个库来操作的话,也不是那么困难。我在ec.h写了一个辅助方法,他的声明如下:

1
EC_KEY *bbp_ec_new_keypair(const uint8_t *priv_bytes);

我们现来一起分析其中的一部分代码。代码涉及到OpenSSL中的一些数据结构:

  • BN_CTX, BIGNUM
  • EC_KEY
  • EC_GROUP, EC_POINT

前两个结构属于OpenSSL中任意精度运算的领域,它们的作用是处理非常大的数字。其它类型都和EC加密有关。EC_KEY可以是完整的密钥对(私钥+公钥),也可以只包含公钥,EC_GROUP和 EC_POINT这两个数据结构就用来帮助我们从私钥计算得出公钥。

最重要的一点是,我们通过创建一个EC_KEY数据结构来操作密钥对:

1
key = EC_KEY_new_by_curve_name(NID_secp256k1);

加载私钥比较容易,但是需要一个中间步骤。在给密钥对提供priv_bytes这个输入之前,我们需要把它转化成一个BIGNUM,这里给它命名为priv:

1
2
3
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。最终我们把公钥也加载到密钥对中:

1
2
3
4
5
6
7
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存放我们的私钥:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
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因为i2o_ECPublicKey会更改输入的指针 */
    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方法来获取的,目的是让pub分配到足够多的字符空间来存放输出。i2o_ECPublicKey最终把公钥从内部定义的数据结构转化为8位字节数组,这既是为什么这个方法的以i2o开头(octet是字节的意思)。这些经编码后的字节最后通过pub_copy零时变量存入key变量。

请读者自己尝试允许这个程序并和openssl在命令行下的输出做对比

Comments