主页 > imtoken苹果版下载 > 为什么选择secp256k1签名算法
为什么选择secp256k1签名算法
签名和验证
本来写了一篇关于以太坊交易签名的文章,但是感觉以太坊的数字签名不够扎实。 这里我们从原理上讲一下以太坊的签名和验证。 希望这篇文章能让你一次性掌握以太坊数字签名技术。
为什么选择secp256k1签名算法
比特币于2009年1月4日成功挖出创世块,至今稳定运行。 出色的稳定运行能力让其他区块链大量借鉴了比特币技术解决方案,包括密码学领域的哈希算法和加密算法。 站在巨人的肩膀上改进技术是我们一贯的做法,以太坊也不例外。 2015年7月30日以太坊上市时,也采用了比特币的签名算法:椭圆曲线算法secp256k1。
secp256k1是由高效密码组标准(SECG)协会制定的一套高效椭圆曲线签名算法标准。 直到比特币流行起来,secp256k1 才真正被使用。 secp256k1命名由几部分组成:sec来自SECG标准,p表示曲线坐标为素数域,256表示素数长度为256位以太坊加密算法,k表示是Koblitz曲线的变体,以及1 表示它是该类型曲线的第一个标准。
SECG(Standards for Efficient Cryptography Group)成立于 1998 年,是一个从事密码标准泛化潜力研究的组织。 旨在促进高效密码学的采用并提高各种计算平台之间的互操作性。
但由于几个不错的功能,它最近越来越受欢迎。 最常用的椭圆曲线是随机结构,但 secp256k1 构建了非随机结构以提高计算效率。 因此,在充分优化算法代码实现后,其计算效率可比其他椭圆曲线算法快30%以上。 此外,与常用的NIST曲线不同,secp256k1的常数是以可预测的方式选取的,可以有效降低曲线设计者安装后门的可能性。
密码学的内容涉及到太多的数学知识,我余双琪无法在这里解释其中的一二三:)。 有兴趣的可以阅读Secp256k1算法标准文档。 这里我只画一张图,让大家了解签名算法的分类。
从图中可以看出,secp256k1是ECDSA算法中的一个标准,出现的比较晚。 为什么中本聪使用比特币secp256k1作为交易验证的签名算法? 比特币开发者社区已经讨论过 secp256k1 是否安全。 中本聪并没有解释清楚,只是说了“有根据的猜测”。 社区里的讨论无非就是安全和效率之间的权衡。 选择不受任何政府控制、没有后门的签名算法是比特币的首要考虑因素。 其次以太坊加密算法,它还需要提供计算速度。 毕竟比特币中的加密签名、签名、验证签名是不断被处理的东西(大约60%的CPU时间几乎全部花在了它们上面),具有可预测性和高计算效率的Koblitz曲线是一个不错的选择. 基于安全第一,效率第二的原则,secp256k1是一个最优解。
以太坊和比特币签名之间的差异
以太坊签名算法虽然是secp256k1,但是签名格式还是有区别的。
比特币在BIP66中对签名数据格式采用了严格的DER编码格式,其签名数据格式如下:
0x30 [total-length] 0x02 [R-length] [R] 0x02 [S-length] [S]
这里0x30和0x02是DER数据格式定义的Tags,不同的Tags对应不同的含义。 对于 secp256k1 算法:
请注意,此处尚未包含签名内容的哈希标志信息。
例如下面的代码使用Go语言版本的Bitcoin对字符串ethereum进行签名,
package main
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"log"
"github.com/btcsuite/btcd/btcec"
)
func main() {
dataHash := sha256.Sum256([]byte("ethereum"))
// 准备私钥
pkeyb,err :=hex.DecodeString("289c2857d4598e37fb9647507e47a309d6133539bf21a8b9cb6df88fd5232032")
if err!=nil{
log.Fatalln(err)
}
// 基于secp256k1的私钥
privk,_:=btcec.PrivKeyFromBytes(btcec.S256(),pkeyb)
// 对内容的 hash 进行签名
sigInfo,err:= privk.Sign(dataHash[:])
if err!=nil{
log.Fatal(err)
}
// 获得DER格式的签名
sig :=sigInfo.Serialize()
fmt.Println("sig length:",len(sig))
fmt.Println("sig hex:",hex.EncodeToString(sig))
}
执行代码,输出如下:
sig length 70
sig hex: 304402207912f50819764de81ab7791ab3d62f8dabe84c2fdb2f17d76465d28f8a968f73022055fbb6cd8dfc7545b6258d4b032753b2074232b07f3911822b37f024cd101166
从下图我们可以清楚的看到,对于secp256k1的签名,比特币签名是用DER格式编码的。
在以太坊中对内容进行签名时,还没有进行DER格式。 同样在以太坊中,字符串 ethereum 被签名。
package main
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"log"
"github.com/ethereum/go-ethereum/crypto"
)
func main() {
dataHash := sha256.Sum256([]byte("ethereum"))
// 准备私钥
pkeyb,err :=hex.DecodeString("289c2857d4598e37fb9647507e47a309d6133539bf21a8b9cb6df88fd5232032")
if err!=nil{
log.Fatalln(err)
}
// 基于secp256k1的私钥
pkey,err:=crypto.ToECDSA(pkeyb)
if err!=nil{
log.Fatalln(err)
}
// 签名
sig,err:= crypto.Sign(dataHash[:],pkey)
if err!=nil{
log.Fatal(err)
}
fmt.Println("sig length:",len(sig))
fmt.Println("sig hex:",hex.EncodeToString(sig))
}
执行代码,输出如下:
sig length: 65
sig hex: 7912f50819764de81ab7791ab3d62f8dabe84c2fdb2f17d76465d28f8a968f7355fbb6cd8dfc7545b6258d4b032753b2074232b07f3911822b37f024cd10116600
与比特币签名相比,以太坊的签名格式是r+s+v。 r和s为ECDSA签名的原始输出,最后一个字节为recovery id值,但在以太坊中用V表示,v的值为1或0。recovery id简称为recid,表示从内容和签名中成功恢复出公钥后需要搜索的次数(因为根据r值,椭圆曲线中可能有多个符合要求的坐标点),但是最大需要搜索的次数在比特币下两次。 这样,在签名验证还原公钥时,无需遍历查找,一次就可以找到公钥,加快了签名验证的速度。
以太坊中的签名代码实现如下:
//crypto/signature_nocgo.go:60
func Sign(hash []byte, prv *ecdsa.PrivateKey) ([]byte, error) {
if len(hash) != 32 {//❶
return nil, fmt.Errorf("hash is required to be exactly 32 bytes (%d)", len(hash))
}
if prv.Curve != btcec.S256() {//❷
return nil, fmt.Errorf("private key curve is not secp256k1")
}
//❸
sig, err := btcec.SignCompact(btcec.S256(), (*btcec.PrivateKey)(prv), hash, false)
if err != nil {
return nil, err
}
// Convert to Ethereum signature format with 'recovery id' v at the end.
v := sig[0] - 27 //❹
copy(sig, sig[1:])//❺
sig[64] = v
return sig, nil
}
下图为上述操作签名数据转换的示例流程。 第一次查找只找到合法的公钥,所以recid为零。
需要注意的一点是,以太坊的crypto.Sign函数实际上使用了两个代码库,C语言版本和Go语言版本。 那么实际对外调用secp256k1时调用的是哪个语言版本呢? 这是在编译时确定的。 如下图所示,以太坊的签名函数提供了C版本的调用和纯Go的调用。 两种语言版本都会在文件开头标明编译条件和文件名,以示区分。 上面的解析代码是比特币secp256k1 Go语言版本调用的。 语言存储库是 github.com/btcsuite/btcd/btcec。
cgo允许Go语言跨语言调用C,可以将Go代码和C代码打包在一起。 如果想了解更多,请参考官方文章C? 去? 加油!
签名验证
使用crypto.Sign 对内容进行签名后,还可以使用crypto.VerifySignature 方法验证签名是否正确。 以下示例代码演示了对上述示例中得到的签名结果的验证。
func main() {
decodeHex:= func(s string) []byte {
b,err:=hex.DecodeString(s)
if err!=nil{
log.Fatal(err)
}
return b
}
dataHash := sha256.Sum256([]byte("ethereum"))
sig:=decodeHex(
"7912f50819764de81ab7791ab3d62f8dabe84c2fdb2f17d76465d28f8a968f7355fbb6cd8dfc7545b6258d4b032753b2074232b07f3911822b37f024cd10116600")
pubkey:=decodeHex(
"037db227d7094ce215c3a0f57e1bcc732551fe351f94249471934567e0f5dc1bf7")
ok:=crypto.VerifySignature(pubkey,dataHash[:],sig[:len(sig)-1])
fmt.Println("verify pass?",ok)
}
关键是在调用验证签名函数时,第三个参数sig用sig[:len(sig)-1]发送,去掉末尾的一个字节。 这是因为函数VerifySignature要求sig参数必须是[R][S]格式,所以需要去掉末尾的[V]。
链上数据签名与验证
以上签名只是对secp256k1的签名和验证。 但实际上,在区块链中,为了安全起见,在签名中加入了特征数据,如签名类型(环签名、单一私钥签名等)、链标识符等。在以太坊中,区块中的数据是只有交易需要签名,所以我就以交易为例来说明以太坊的链数据签名和交易。
交易数据签名
以太坊加密算法采用比特币的椭圆曲线secp256k1加密算法。 签名交易对应的代码如下:
//core/types/transaction_signing.go:56
func SignTx(tx *Transaction, s Signer, prv *ecdsa.PrivateKey) (*Transaction, error) {//❶
h := s.Hash(tx)//❷
sig, err := crypto.Sign(h[:], prv)//❸
if err != nil {
return nil, err
}
return tx.WithSignature(s, sig)//❹
}
这样,一个签名的交易只能属于某个唯一的区块链。
根据以上代码逻辑,提取出如下交易签名流程。 整个过程使用了RLP编码、Keccak256哈希算法和椭圆曲线secp256k1加密算法。 从这里可以看出,密码学技术是区块链成功的最大基石。
上图中还有一个关键数据,Signer是如何生成R、S、V值的。 从前面的签名算法过程可以知道,R和S是ECDSA签名的原始输出,V的值为recid,其值为0或1。但是当交易被签名时,V的值不再是recid,但 recid+ chainID*2+ 35。例如:
tx:=types.NewTransaction(1,
common.HexToAddress("0x002e08000acbbae2155fab7ac01929564949070d"),
big.NewInt(100),21000,big.NewInt(1),nil)
创建一个交易,用私钥 289c2857d4598e37fb9647507e47a309d6133539bf21a8b9cb6df88fd5232032 签名。
// 实例化一个签名器
signer:=types.NewEIP155Signer(big.NewInt(888))
tx,err=types.SignTx(tx,signer,pkey)
if err!=nil{
log.Fatalln(err)
}
v,r,s:=tx.RawSignatureValues()
fmt.Printf("tx sign V=%d,R=%d,S=%d\n",v,r,s)
得到 V = 888*2+recid+35= 1812。
交易签名分析流程
签署交易后,如何获得交易签署人? 这是加密算法的逆向解signer,利用用户的签名内容和签名信息(R,S,V)得到用户私钥的公钥,从而得到signer的账户地址。 详情如下所示。
与交易签名过程相比,解决方案签名是一种反向推导。
//core/types/transaction_signing.go:127
func (s EIP155Signer) Sender(tx *Transaction) (common.Address, error) {
if !tx.Protected() { //❶
return HomesteadSigner{}.Sender(tx)
}
if tx.ChainId().Cmp(s.chainId) != 0 { //❷
return common.Address{}, ErrInvalidChainId
}
V := new(big.Int).Sub(tx.data.V, s.chainIdMul)//❸
V.Sub(V, big8)
return recoverPlain(s.Hash(tx), tx.data.R, tx.data.S, V, true)
}
也就是说考虑了27或者28的老签名方式,超过了MaxUint64。 得到chainID后,判断tx.ChainID是否等于当前网络的ChainID。
至此,我们讲解了以太坊的签名以及与比特币的区别,最后讲解了以太坊中一笔交易的签名过程和验证签名过程。