既然我们已经可以产生椭圆曲线密钥对,我们接下来就用使用它来进行消息的签名和验证。我所指的消息是任何形式,无论是文本还是二进制形式,只要它们有被验证合法性的需要。特别的是,bitcoin客户端通过签名来证明交易的有效性,反之,矿工则是通过验证这样的签名,来批准并广播合法的交易。
ECDSA 签名
椭圆曲线签名算法就是ECDSA(Elliptic-Curve Digital Signature Algorithm).在ECDSA中,各方必须约定一个共同的哈希函数H
, 因为我们将要签名的对象是H(消息)
,而不是消息本身。值得注意的是,只有签名方S有私钥的权限,验证方V只需要拿到相应的公钥就可以进行验证。本文中,我将使用上一章所创建的密钥对。
下面的案例中,我们签名的对象是SHA-256
摘要。但bitcoin中指定的H
函数是HASH256
,也就是指双重SHA-256
。
签名
第一步把我们的消息存入文件,命名ex-message.txt
。
This is a very confidential message
之后,我们用私钥对其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
为了重用刚刚输出的结果,最好使用hexdup
已生成的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[] = "This is a very confidential 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;
使用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);
验证
验证同样很简单,可以在ex-ecdsa-verify.c
中查看:
uint8_t pub_bytes[33] = { ... };
uint8_t der_bytes[] = { ... };
const char message[] = "This is a very confidential 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_KEY *bbp_ec_new_pubkey(const uint8_t *pub_bytes, size_t pub_len);
另一方面,der_bytes
是签名程序返回的DER
格式的签名。我们将解码DER
签名到更方便的ECDSA_SIG
结构中,然后与消息摘要比较进行验证。
ECDSA_do_verify
函数的返回值:
- 1,签名合法
- 0,签名不合法
- -1,出现未知错误
注意:使用ECDSA_verify
可以跳过签名的解码过程,因为它需要的输入值是DER
形式的签名。