数字签名是什么数字签名,简单讲,就是一种证明「这份数据是我发的」的方法。本质上,就是用私钥去对一段消息去签名,对方用公钥去验证这份签名,证明这份私钥是由我发送的并且消息没有遭到篡改:https://learnblockchain.cn/shawn_shaw
数字签名,简单讲,就是一种 证明「这份数据是我发的」 的方法。本质上,就是用私钥去对一段消息去签名,对方用公钥去验证这份签名,证明这份私钥是由我发送的并且消息没有遭到篡改。 在以太坊上,使用到的数字签名(加密)算法是 ECDSA。
身份验证 数字签名能证明:消息确实是由某个持有私钥的人发出的。因为只有私钥持有者才能生成正确的签名,别人伪造不了。
消息完整
签名是基于消息的哈希生成的。如果消息内容哪怕只改动一丁点(哪怕一个标点符号),hash
就变了,签名也验证不了。
不可抵赖 一旦签了名,就无法否认自己签过。因为只有你拥有私钥,签名是你自己产生的,别人通过签名和消息可以恢复出来你的身份,别人无法伪造。
指得是原始的数据消息。例如一段字符串数据
Hello World
指的是原始数据经过 hash
算法生成的 32
byte
的数据。在以太坊中,这个消息会经过两次 keccak()
的算法进行生成。
hash
// 1. 直接把字符串hash
bytes32 messageHash = keccak256(bytes(message));
hash
加上这个字符串标志的作用是防止重放攻击。细节我们在下面转化消息的步骤详解。
return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash));
ECDSA
私钥本身,是一个 32
字节的大整数。生成方式如下(go-ethereum
库)
privateKey, err := crypto.GenerateKey()
生成结果为:
0x9db7a2287bf11462793f2b5c726d12ce79f2e25fbc9d08316b55386061e35a4c
公钥是由私钥进行生成的。有两种公钥,压缩公钥和未压缩公钥。以太坊中使用的公钥为未压缩公钥。
未压缩公钥:
结构为:0x04
+ X坐标
+ Y 坐标
。
大小为:uint8
+ bytes32
+ bytes32
= 65
字节
如:
0x049a9c81e4f44f721e610a9c3cb3c2f6a8ca57b142309ba5c51d6b135988594d8fe3e5c0c5649f413f62de7c3c3e3b4974600bc517a637da51967b15e79b3d9252
0x02
或0x03
+ X 坐标
如:0x021e2dda68120487e0807300c0f6e8e7d9e974a40f59ae3060d2ae6077fa66c092
在以太坊中,地址格式为未压缩公钥去掉 0x04后
,经过 keccak()
哈希取后 20
字节(40
个 16
进制字符)而产生的。
如
0x49755928d2471581649f356f2a414e36d334055d
签名本质上也是一个 65
字节的字节序列,分别是有三个值(r、s、v)
,大小分别为 32
字节、32
字节、1
字节。r、s、v
简单拼接起来就是一个完整的签名消息。
如:
0x
2c6404e1145f38a8eb7b6a5d7648e6a711f3dfd32e7d7fa8a6c4b17e6b6c6b6d // r (32字节)
56c4f0a1b0494c6578723e1c5e8f302ff7f6ba674fbec6d1571318c43259b2e4 // s (32字节)
1b // v (1字节)
在以太坊中,可以通过 r
、s
、v
值以及以太坊签名消息来进行恢复出来公钥(可推导出地址)来进行验证。
如:
// 恢复 signer
address recovered = ECDSA.recover(ethSignedHash, v, r, s);
以太坊中使用到的签名算法是 ECDSA
(一种基于椭圆曲线的签名算法),使用到的椭圆曲线是 secp256k1
。
签名流程为:
Hash
用 keccak256
处理原始消息,得到 messageHash
。
(如果是普通签名,还会加一段 \x19Ethereum Signed Message:\n32
前缀)messageHash
签名
使用 secp256k1
曲线和 ECDSA
签名算法,生成 (r, s, v)
。r || s || v
按顺序拼接成 65
字节数据。
hash
)
// 1. 直接把字符串hash
bytes32 messageHash = keccak256(bytes(message));
第一次 hash 是直接对原始交易的数据进行使用 keccak() 函数进行 hash 化。
hash
)
return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash));
在这一步中,对第一步获得的 hash
值添加以太坊的字符串标识再进行一次 hash
。通过添加以太坊的标识,明确告诉链上验证者「这是人为签名的消息,不是链上的交易或指令」。如果不加前缀,攻击者可以拿着你签名过的数据,在链上伪造一些危险的行为!这就是签名可重放攻击,例如:
from: 0xaaa...aaa
to: 0xbbb...bbb
value: 1 ETH
nonce: 5
gasLimit: 21000
gasPrice: 20 gwei
chainId: 1
假设我们有这一份链上交易数据。正常来说,直接使用 keccak256(hash)
后进行签名是可以发上以太坊网络进行交易发起的。
那么,假如此时有攻击者让你签了这个数据,拿到这个交易后直接发送到区块链网络,即发起了一次签名重放攻击。你的资金就发生了损失。(因为 signature
可以证明原交易数据的完整性)
但是,如果进行二次哈希,在第二次计算 hash
的时候,添加上以太坊的字符串。那么,这个签名则会被认为是离线签名,无法进行执行交易等危险行为。(因为最终 signature
只能保证 \x19Ethereum Signed Message:\n32 + hash
的完整性, 而不是可执行交易数据的完整性)
总得来说,二次签名并加上以太坊字符串标识是为了区分 链上交易 和 链下签名数据 两种类型。防签名重放攻击则是为了避免签名被使用在链上交易上面,避免资金丢失。
foundry
的 vm
来进行模拟签名。
通过使用私钥对消息 hash
进行签名,可以获取签名信息,也就是 r、s、v
三个值。
// 3. 签名,这一步和之前的步骤都发生在链下
(uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, ethSignedHash);
openzepplin
的 ECDSA
库,参数为: 消息哈希、r、s、v
。 // 4. 恢复 signer
address recovered = ECDSA.recover(ethSignedHash, v, r, s);
recovered
(一个地址),我们就可以进行签名人匹配,如果相等,则说明消息完整且发起者为 signer
。
assertEq(recovered, signer, "Recovered signer does not match");
contract TestSignVerifyOZ is Test {
using ECDSA for bytes32;
address signer;
uint256 privateKey;
function setUp() public {
privateKey = 123456789;
signer = vm.addr(privateKey);
}
function testSignAndRecoverOZ() public {
string memory message = "hello world";
// 1. 直接把字符串hash
bytes32 messageHash = keccak256(bytes(message));
// 2. 用 OZ 帮我们加前缀(标准 Ethereum Signed Message 格式)
bytes32 ethSignedHash = MessageHashUtils.toEthSignedMessageHash(messageHash);
console.log("message is:");
console.logBytes32( ethSignedHash );
// 3. 签名,这一步和之前的步骤都发生在链下
(uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, ethSignedHash);
console.log("signature is:");
console.logBytes( abi.encodePacked(r,s,v) );
// 4. 恢复 signer
address recovered = ECDSA.recover(ethSignedHash, v, r, s);
console.log("recovered is:");
console.logAddress( recovered );
console.log("signer is:");
console.log(signer);
assertEq(recovered, signer, "Recovered signer does not match");
}
}
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!