数字签名算法:
以太坊使用的数字签名算法叫双椭圆曲线数字签名算法(ECDSA),基于双椭圆曲线“私钥-公钥”对的数字签名算法。它主要起到了三个作用:
身份认证
:证明签名方是私钥的持有人
不可否认
:发送方不能否认发送过这个消息
完整性
:消息在传输过程中无法被修改
流程:
签名交易:
签名的类型:
eth_sign
是用来签署任意数据。这使得它是最强大的,最简单的(只是签署数据),但也是最危险的。
这里的问题是,你可以让用户签署一个数据,而这个实际上是交易数据。想象一下,你让用户登录到你的服务,但你让他们签署的数据实际上是一个交易,如 “发送5个ETH给攻击者”。
交易毕竟只是由字节组成,人们很可能不会检查他们所签署的这串字符的实际含义。看似无害的签名,却成了窃取资金的攻击。所以一般不鼓励直接使用eth_sign
。
personal_sign
后来加入来解决这个问题。该方法在任何签名数据前加上"\x19Ethereum Signed Message:\n"
,这意味着如果有人要签署交易数据,添加的前缀字符串会使其成为无效交易。
对于更复杂的用例,特别是在智能合约中使用时,EIP-712标准被创建。EIP-712标准随着时间的推移而有所改变,但目前MetaMask支持的最后一个版本是signTypedData_v4
。或者你可以使用一个特定的库,如eip-712。
EIP-712解决的主要问题是确保用户清楚地知道他们在签署什么,为哪个合约地址和网络签署,而且每个签名最多只能使用一次。简而言之,这是通过签署所有需要的配置数据(地址、chain id、版本、数据类型)的哈希值+实际数据本身来实现的。ERC20-Permit 是一个关于如何使用signTypedData_v4的好例子。
实例:
新地址刚登录opensea时,就会弹出签名。下图是小狐狸(metamask)钱包进行签名时弹出的窗口,它可以证明你拥有私钥的同时不需要对外公布私钥。
创建签名:
打包信息:
在以太坊的ECDSA标准中,被签名的消息是一组数据的keccak256哈希
,为bytes32类型
。
我们可以把任何想要签名的内容利用abi.encodePacked()
函数打包,然后用keccak256()
计算哈希,作为消息。
我们例子中的消息
由string类型变量得出:
1 2 3 4
| function getSingleMessageHash(string memory _message) public pure returns (bytes32){ return keccak256(abi.encodePacked(_message)); }
|
1 2 3 4 5 6 7 8 9 10
| describe("Verify sign", function () { it.only("generate sign", async function () { const {Verify, users, tokenOwner} = await setup(); const message = "hello"; let result = await Verify.getSingleMessageHash(message); console.log("messageHash: " + result); }); });
|
1 2
| messageHash: 0x1c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8
|
打包信息 = abi.encodePacked()
打包 + keccak256()
计算哈希
计算以太坊签名消息:
消息
可以是能被执行的交易,也可以是其他任何形式。
为了避免用户误签了恶意交易,ethereum PR2940提倡在消息前加上"\x19Ethereum Signed Message:\n32"
字符,并再做一次keccak256哈希
,作为以太坊签名消息。
1 2 3 4 5
| function toEthSignedMessageHash(bytes32 hash) public pure returns (bytes32) { return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)); }
|
1 2
| sig: 0x456e9aea5e197a1f1af7a3e85a3212fa4049a3ba34c2289b4c860fc0b0c64ef3
|
对消息签名 = 签名hash+固定字符串后abi.encodePacked()
打包 + keccak256()
计算哈希
钱包进行签名:
日常操作中,大部分用户都是通过这种方式进行签名。
在获取到需要签名的消息之后,我们需要使用metamask钱包进行签名。
metamask的personal_sign
方法会自动把消息转换为以太坊签名消息,然后发起签名。
所以我们只需要输入消息和签名者钱包account即可。
需要注意的是输入的签名者钱包account需要和metamask当前连接的account一致才能使用。
1 2 3 4 5 6
|
ethereum.enable() account = "0xdD2FD4581271e230360230F9337D5c0430Bf44C0" hash = "0x1c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8" ethereum.request({method: "personal_sign", params: [account, hash]})
|
在控制台进行输入:
在返回的结果中(Promise的PromiseResult
)可以看到创建好的签名。不同账户有不同的私钥,创建的签名值也不同。利用上面私钥创建的签名如下所示:
1
| 0x606a2bb1fa3459b622a37b54a88b051ad8d3e0c266373ca3d9a9da3c1b05e0dd4c5c647d72762cb65628ee377a3b9fb1ac09f157d5ae2150c07258117e8a24ae1c
|
利用ethersjs签名:
测试代码地址: https://github.com/jerrychan807/my-awesome-solidity/blob/main/easy_sign/contracts/Verify.sol
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| describe("Verify sign", function () { it.only("generate sign", async function () { const {Verify, users, tokenOwner} = await setup(); const provider = new ethers.providers.JsonRpcProvider("http://localhost:8545"); let privateKey = '0xde9be858da4a475276426320d5e9262ecfc3ba460bfac56360bfa6c4c28b4ee0'; let wallet = new ethers.Wallet(privateKey);
const message = "hello"; console.log("message: " + message); const messageHash = ethers.utils.solidityKeccak256(["string"], [message]); const messageHashByte = ethers.utils.arrayify(messageHash); let signature = await wallet.signMessage(messageHashByte); console.log("signature: " + signature); }); });
|
测试结果如下,与Metamask的消息生成的签名一致。
1 2
| message: hello signature: 0x606a2bb1fa3459b622a37b54a88b051ad8d3e0c266373ca3d9a9da3c1b05e0dd4c5c647d72762cb65628ee377a3b9fb1ac09f157d5ae2150c07258117e8a24ae1c
|
验证签名:
为了验证签名,验证者需要拥有:
我们能验证签名的原因是只有私钥的持有者才能够针对交易生成这样的签名,而别人不能。
1 2 3 4 5 6
| function verify(address _signer, string memory _message, uint8 _v, bytes32 _r, bytes32 _s) public pure returns (bool) { bytes32 msgHash = keccak256(abi.encodePacked(_message)); bytes32 msgDigest = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", msgHash)); return ecrecover(msgDigest, _v, _r, _s) == _signer; }
|
签名中包含r, s, v
三个值的信息,通过签名、消息恢复出 签名者的地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| describe("Verify sign", function () { it.only("generate sign", async function () { const {Verify, users, tokenOwner} = await setup(); const provider = new ethers.providers.JsonRpcProvider("http://localhost:8545"); let privateKey = '0xde9be858da4a475276426320d5e9262ecfc3ba460bfac56360bfa6c4c28b4ee0'; let wallet = new ethers.Wallet(privateKey);
const message = "hello"; console.log("message: " + message); const messageHash = ethers.utils.solidityKeccak256(["string"], [message]); const messageHashByte = ethers.utils.arrayify(messageHash); let signature = await wallet.signMessage(messageHashByte); console.log("signature: " + signature); let sig = ethers.utils.splitSignature(signature); let result = await Verify.verify(wallet.address, message, sig.v, sig.r, sig.s); console.log(result); }); });
|
测试结果:
1 2 3 4
| message: hello signature: 0x606a2bb1fa3459b622a37b54a88b051ad8d3e0c266373ca3d9a9da3c1b05e0dd4c5c647d72762cb65628ee377a3b9fb1ac09f157d5ae2150c07258117e8a24ae1c
true
|
Refs: