比特币开发系列 - 椭圆曲线密钥
在上一篇文章中,我向你概述了公钥密码学及其与区块链的关系。你将开始熟悉用于生成比特币密钥对的真实加密函数。
希望通过尽可能清晰的方式来讨论这个主题,这篇文章可能会显得有点啰嗦。无论如何,你的耐心将会得到回报,因为从这里开始,课程将变得轻松。
关于比特币椭圆曲线密码学的一些事实:
在这个复杂的背景下,我们的唯一输入是私钥。公钥是从私钥唯一派生出来的,无论是未压缩还是压缩形式。首先,我们将使用 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
前缀。将未压缩的公钥转换为压缩形式很容易,我们只需省略 $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 字节:
02
或 03
前缀。密钥对生成任务繁琐,但在 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 数学,这不是本系列的目的。基本上,我们在曲线上定位一个固定点 $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) {
...
}
...
}
...
测试步骤如下:
priv_bytes
初始化一个 EC_KEY
密钥对。BIGNUM
将私钥重新放入 priv
中。pub
中。第三步是最复杂的。首先设置转换形式,这将影响公钥的长度(33 或 65)。通过对 i2o_ECPublicKey
的 NULL
参数调用来获取实际长度,以便为 pub
分配足够的字节来容纳输出。最终,i2o_ECPublicKey
将公钥从内部表示转换为八位字节,因此有 i2o
前缀(octet = byte)。编码字节通过 pub_copy
写入 pub
。
尝试运行程序并将其与 openssl
命令行工具的输出进行比较。
在 GitHub 上查看完整源代码。
你已经学会如何生成新的 EC 密钥对。通过一些自定义代码,你还学会了如何从原始字节创建密钥对。
在下一篇文章中,你将使用 EC 密钥对来生成和验证 数字签名。如果你喜欢这篇文章,请分享。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!