比特币特开发系列 - 椭圆曲线数字签名
现在你已经能够生成 EC 密钥对,下一步是使用它们来签名和验证消息。所谓消息是指任何需要进行身份验证的数据,可以是文本或二进制。具体来说,比特币客户端生成签名以授权其交易,而矿工验证这些签名和广播有效的交易。
本文将处理通用消息。稍后,我们将了解比特币交易签名过程中涉及的消息类型。
毫不奇怪,EC 签名算法是 ECDSA(椭圆曲线数字签名算法)。在 ECDSA 中,所有相关方必须就一个哈希函数 H 达成一致,因为我们将对 H(message) 进行签名,而不是对消息本身进行签名。值得注意的是,只有签名方 S 能够访问私钥,而验证方 V 拥有相应的公钥以验证 S 的签名。我将重复使用上一篇文章中的私钥和公钥。
以下示例使用 SHA-256 摘要,但请记住,比特币指定的 H 函数是 hash256,也称为双 SHA-256(阅读有关哈希的文章 )。
第一步是将我们的消息放入文件中,比如说 ex-message.txt:
This is a very confidential message
其 SHA-256 摘要为(不要忘记结尾的 \n
):
45 54 81 3e 91 f3 d5 be
79 0c 7c 60 8f 80 b2 b0
0f 3e a7 75 12 d4 90 39
e9 e3 dc 45 f8 9e 2f 01
然后,我们使用私钥对消息的 SHA-256 摘要进行签名:
$ openssl dgst -sha256 -sign ec-priv.pem ex-message.txt >ex-signature.der
ex-signature.der文件是_DER_格式的消息签名。OpenSSL 对于任何二进制输出(密钥、证书、签名等)都使用 DER 编码 ,但我将跳过底层细节。你不需要了解 ECDSA 签名的语义,只需记住它是一对大数 $$(r, s)$$。
你可能会注意到每次运行程序时签名都会更改,也就是说,默认的签名过程不是确定性的。当序列化区块链交易时,这可能会成为一个问题,因为签名是交易字节的一部分,你可能记得交易字节哈希到 txid。因此,每次签署交易时,txid 都会更改。这种行为通常是交易延展性问题的一个来源。
要显示十六进制编码的签名,只需添加 -hex
标志:
$ openssl dgst -sha256 -hex -sign ec-priv.pem ex-message.txt
不过,为了获得可重复的输出,最好对 DER 文件进行十六进制转储:
$ hexdump ex-signature.der
每当发布经过身份验证的消息到网络时,读者都希望找到附加的签名。这两个文件是验证例程的输入,前提是我们事先收到了作者的公钥:
$ openssl dgst -sha256 -verify ec-pub.pem -signature ex-signature.der ex-message.txt
如果签名经过验证,我们就能够声明消息是真实的。
下面的代码执行了我们在上一节中通过命令行完成的操作。
OpenSSL 使签名操作变得简单,看看 ex-ecdsa-sign.c:
uint8_t priv_bytes[32] = { ... };
const char message[] = "这是一条非常机密的消息\n";
EC_KEY *key;
uint8_t digest[32];
ECDSA_SIG *signature;
uint8_t *der, *der_copy;
size_t der_len;
...
key = bbp_ec_new_keypair(priv_bytes);
bbp_sha256(digest, (uint8_t *)message, strlen(message));
signature = ECDSA_do_sign(digest, sizeof(digest), key);
其中 ECDSA_SIG
是一个简单的结构,保存了前面段落中描述的 \((r, s)\) 对:
struct {
BIGNUM *r;
BIGNUM *s;
} ECDSA_SIG;
我的测试输出(你的输出将不同):
r: 2B2B529BDBDC93E78AF7E00228B179918B032D76902F74EF454426F7D06CD0F9
s: 62DDC76451CD04CB567CA5C5E047E8AC41D3D4CF7CB92434D55CB486CCCF6AF2
通过 i2d_ECDSA_SIG
函数,我们还可以获得 DER 编码的签名:
der_len = ECDSA_size(key);
der = calloc(der_len, sizeof(uint8_t));
der_copy = der;
i2d_ECDSA_SIG(signature, &der_copy);
在我的测试中,它整洁地呈现如下(你能找到里面的 r
和 s
吗?):
30 44
02 20
2b 2b 52 9b db dc 93 e7
8a f7 e0 02 28 b1 79 91
8b 03 2d 76 90 2f 74 ef
45 44 26 f7 d0 6c d0 f9
02 20
62 dd c7 64 51 cd 04 cb
56 7c a5 c5 e0 47 e8 ac
41 d3 d4 cf 7c b9 24 34
d5 5c b4 86 cc cf 6a f2
验证签名也很简单,这里是 ex-ecdsa-verify.c:
uint8_t pub_bytes[33] = { ... };
uint8_t der_bytes[] = { ... };
const char message[] = "这是一条非常机密的消息\n";
EC_KEY *key;
const uint8_t *der_bytes_copy;
ECDSA_SIG *signature;
uint8_t digest[32];
int verified;
...
key = bbp_ec_new_pubkey(pub_bytes);
der_bytes_copy = der_bytes;
signature = d2i_ECDSA_SIG(NULL, &der_bytes_copy, sizeof(der_bytes));
由于我们没有私钥,我们必须将 pub_bytes
解码为一个压缩的公钥,使用来自 ec.h 的以下辅助函数:
EC_KEY *bbp_ec_new_pubkey(const uint8_t *pub_bytes, size_t pub_len);
另一方面,der_bytes
是签名程序返回的 DER 编码的签名。我们将 DER 签名解码为方便的 ECDSA_SIG
结构,然后对同一 SHA-256 消息摘要进行验证:
bbp_sha256(digest, (uint8_t *)message, strlen(message));
verified = ECDSA_do_verify(digest, sizeof(digest), signature, key);
ECDSA_do_verify
函数返回:
1
。0
。-1
。注意:可以通过使用 ECDSA_verify
跳过签名解码,该函数直接接受 DER 编码的签名。
在 GitHub 上查看完整源代码。
你学会了如何使用私钥对消息进行签名,以及如何使用公钥验证消息签名。
这是关于通用加密的最后一篇文章,呼!在下一篇文章中,我将介绍比特币网络。如果你喜欢这篇文章,请分享。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!