签名与校验 :: 以太坊技术与实现

您所在的位置:网站首页 比特币中会将私钥使用什么编码方式对私钥进行转换 签名与校验 :: 以太坊技术与实现

签名与校验 :: 以太坊技术与实现

2024-07-15 15:36| 来源: 网络整理| 查看: 265

我要编辑 > 底层核心技术 > 签名与校验签名与校验

原本写有以太坊交易签名的文章,但觉得对以太坊的数字签名还讲得不够夯实。这里从原理上聊聊以太坊签名与校验,希望这篇文章让你一次性掌握以太坊数字签名技术。

为何选择签名算法 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数据格式中定义的Tag,不同Tag对应不同含义。以 secp256k1 算法来说:

total-length: 1字节,表示签名字节总长度,其值等于:4byte(Tag total-length 后面的四个Tag)+R的长度+S的长度。而secp256k1算法是256长度,即32字节。因此签名字节总长度为0x44(68=4+32+32)。R-length: 1字节,表示R值长度。其值始终等于0x20(表示十进制32)。R: 32字节,secp256k1 算法中的R值。S-length: 1字节,表示S值长度。始终等于0x20。S:32字节,secp256k1 算法中的S值。

注意,这里还尚未包含签名内容的哈希标志信息。

例如,如下代码是利用Go语言版的比特币对字符串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 }❶ 首先,签名是针对32字节的byte,实际上是对应待签名内容的哈希值,以太坊中哈希值common.Hash长度固定为32。比如对交易签名时传入的是交易哈希crypto.Sign(tx.Hash()[:], prv)。❷ 确保私钥的曲线算法是比特币的secp256k1。目的是控制所有签名均通过 secp256k1 算法计算。❸ 调用比特币的签名函数,传入 secp256k1 、私钥和签名内容,并说明并非压缩的私钥。此时 SignCompact 函数返还一定格式的签名。其格式为:[27 + recid] [R] [S]❹ 以太坊将比特币中记录的recovery id 提取出。减去27的原因是,比特币中第一个字节的值等于27+recid,因此 recid= sig[0]-27。❺ 以太坊签名格式是[R] [S] [V],和比特币不同。因此需要进行调换,将 R 和 S 值放到前面,将 recid 放到最后。

下图中展示的上面操作签名数据转换示例流程。只是在第一次查找便查找到合法公钥,因此 recid 为零。 以太坊签名数据格式

有一点需要注意,以太坊的 crypto.Sign 函数实际是采用两个代码库,C语言版和Go语言版。那么在外部在实际调用时调用的是哪个语言版本的secp256k1呢?这在编译期由编译约束条件决定。如下图以太坊的签名函数提供了C版调用和纯Go调用,两个语言版本在文件开头会标记编译条件和文件名上做区分,上面的解析代码属比特币 secp256k1 Go语言版调用,其Go语言库是 github.com/btcsuite/btcd/btcec 。

以太坊crypto签名调用提供CGo和GO调用

cgo 能让Go语言跨语言调用 C ,可以将Go代码和C代码打包到一起,想了解更多可参加官方文章 C? Go? Cgo!

签名校验

使用使用 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)//❹ }

❶ 交易签名时,需要提供一个签名器(Signer)和私钥(PrivateKey)。需要Singer是因为在EIP155修复简单重复攻击漏洞后,需要保持旧区块链的签名方式不变,但又需要提供新版本的签名方式。因此通过接口实现新旧签名方式,根据区块高度创建不同的签名器。

//core/types/transaction_signing.go:42 func MakeSigner(config *params.ChainConfig, blockNumber *big.Int) Signer { var signer Signer switch { case config.IsEIP155(blockNumber): signer = NewEIP155Signer(config.ChainID) case config.IsHomestead(blockNumber): signer = HomesteadSigner{} default: signer = FrontierSigner{} } return signer }

❷ 重点介绍EIP155改进提案中所实现的新哈希算法,主要目的是获取交易用于签名的哈希值 TxSignHash。和旧方式相比,哈希计算中混入了链ID和两个空值。注意这个哈希值 TxSignHash 在EIP155中并不等同于交易哈希值。

以太坊交易签名内容哈希新补充

这样,一笔已签名的交易就只可能属于某一确定的唯一一条区块链。

❸ 内部利用私钥使用secp256k1加密算法对TxSignHash签名,获得签名结果sig。

❹ 执行交易WithSignature方法,将签名结果解析成三段R、S、V,拷贝交易对象并赋值签名结果。最终返回一笔新的已签名交易。

func (tx *Transaction) WithSignature(signer Signer, sig []byte) (*Transaction, error) { r, s, v, err := signer.SignatureValues(tx, sig) if err != nil { return nil, err } cpy := &Transaction{data: tx.data} cpy.data.R, cpy.data.S, cpy.data.V = r, s, v return cpy, nil }

根据上面代码逻辑,提炼出如下交易签名流程,整个过程利用了 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。

交易签名解析流程

签名交易后,如何才能获得交易签名者呢?这个是加密算法的逆向解签名者,是利用用户签名内容以及签名信息(R、S、V)得到用户私钥的公钥,从而得到签名者账户地址。具体细节如下。

对比交易签名流程,解签名是逆向推导。

//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) }

❶EIP155 下交易属于受保护交易,如果不受保护,则说明属于旧的签名格式,使用HomesteadSigner校验。

交易是否受保护取决于是否是 EIP155 签名器签名,因为在 EIP155 中 v = recid+ chainID*2+ 35,旧算法是 v= recid+27,而 recid 为0或者1,即 v 为 27 或28。因此只要 v 值不等于 27和28则为受保护的交易。

//core/types/transaction.go:117 func isProtectedV(V *big.Int) bool { if V.BitLen()


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3