为什么签名前要加"\x19Ethereum Signed Message:\n"
在以太坊中,可以找到关于上述破损的解释例子。以太坊有两种消息,交易𝕋和字节串𝔹⁸ⁿ。这些分别用eth_sendTransaction
和eth_sign
来签名。最初的编码函数encode : 𝕋∪𝔹⁸ⁿ→𝔹⁸ⁿ
如下定义:
encode(t : T) = RLP_encode(t)
encode(b : 𝔹⁸ⁿ) = b
独立来看的话,它们都满足要求的属性,但是合在一起看就不满足了。如果我们采用b = RLP_encode(t)
就会产生碰撞(即两种编码得到的结果就是一样的了)。在Geth PR 2940中,通过修改编码函数的第二条定义,这种情况得到了缓解:
即在第二种编码方式前加上"\x19Ethereum Signed Message:\n" + len(message)
所以最后的结果是
This PR includes several things:
the behavior of eth_sign is changed. It now accepts an arbitrary message, prepends a known message, hashes the result using keccak256 it calculates the signature of the hash (breaks backwards compatability!).
add personal_sign, same semantics as eth_sign but also accepts a password. The private key used to sign the hash is temporary unlocked in the scope of the request.
add personal_recover, returns the address for the account that created the signature.
personal_sign(hash, address [, password])
即web3.eth.personal.sign(dataToSign, address, password [, callback])
personal_recover(message, signature)
即web3.eth.personal.ecRecover(dataThatWasSigned, signature [, callback])
The known message added to the input before hashing is "\x19Ethereum Signed Message:\n" + len(message).
举例:
内部编码
web3.eth.personal.sign("Hello world", "0x11f4d0A3c12e86B4b5F39B213F7E19D048276DAe", "test password!")
.then(console.log);
> "0x30755ed65396facf86c53e6217c52b4daebe72aa4941d89635409de4c9c7f9466d4e9aaec7977f05e923889b33c0d0dd27d7226b6e6f56ce737465c5cfd04be400"
web3.eth.personal.ecRecover("Hello world", "0x30755ed65396facf86c53e6217c52b4daebe72aa4941d89635409de4c9c7f9466d4e9aaec7977f05e923889b33c0d0dd27d7226b6e6f56ce737465c5cfd04be400").then(console.log);
> "0x11f4d0A3c12e86B4b5F39B213F7E19D048276DAe"
encode(b : 𝔹⁸ⁿ) = "\x19Ethereum Signed Message:\n" ‖ len(b) ‖ b
其中len(b)
是b
中字节数的ASCII十进制编码。
这就解决了两个定义之间的冲突,因为RLP_encode(t : 𝕋)
永远不会以\x19
作为开头。但新的编码函数依然存在确定性和单射性风险,仔细思考这些对我们很有帮助。
原来,上面的定义并不具有确定性。对一个4个字节大小的字符串b
来说,用len(b) = "4"
或者len(b) = "004"
都是有效的。我们可以进一步要求所有表示长度的十进制编码前面不能有0并且len("")="0"
来解决这个问题。
上面的定义并不是明显无碰撞阻力的。一个以"\x19Ethereum Signed Message:\n42a…"
开头的字节串到底表示一个42字节大小的字符串,还是一个以"2a"
作为开头的字符串?这个问题在 Geth issue #14794中被提出来,也直接促使了trezor不使用这个标准。幸运的是这并没有导致真正的碰撞因为编码后的字节串总长度提供了足够的信息来消除这个歧义。
如果忽略了len(b)
,确定性和单射性就没有那么重要了。重点是,很难将任意集合映射到字节串,而不会在编码函数中引入安全问题。目前对eth_sign
的设计仍然将字节串作为输入,并期望实现者提供一种编码。