PersonalMessage & CIP23
简介
conflux-sdk及fluent支持脱胎于以太坊EIP712
的CIP23
。 CIP23
的提出是为了提高签名的安全性。在支持CIP23
前,若签名的消息是一串字符串,可以看到具体的内容,但若签名的消息是一个结构化的数据(typedData),则用户在钱包中看到的消息是一串处理过的哈希字串,没法看到具体的信息;
在支持CIP23
后,用户可以在钱包中看到自己签名的内容(如图1-2支持EIP712
前后metamask的签名所示)。
本文在此基础上介绍CIP23
的基本概念,并以java-conflux-sdk
为例,说明如何实现CIP23
协议下的签名与验签。
基本概念
CIP23
的基本概念与和EIP712
之间的异同可以参考:https://github.com/Conflux-Chain/CIPs/blob/master/CIPs/cip-23.md
CIP23
中主要用于签名的两个方法为personal_sign
与cfx_signTypedData_v4
。前者为实现普通的字符串的签名,如"v0G9u7huK4mJb2K1", 后者为实现结构体的签名,如
{
"types": {
"CIP23Domain": [
{
"name": "name",
"type": "string"
},
{
"name": "version",
"type": "string"
},
{
"name": "chainId",
"type": "uint256"
},
{
"name": "verifyingContract",
"type": "address"
}
],
"Person": [
{
"name": "name",
"type": "string"
},
{
"name": "wallet",
"type": "address"
}
],
"Mail": [
{
"name": "from",
"type": "Person"
},
{
"name": "to",
"type": "Person"
},
{
"name": "contents",
"type": "string"
}
]
},
"primaryType": "Mail",
"domain": {
"name": "Ether Mail",
"version": "1",
"chainId": 1,
"verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
},
"message": {
"from": {
"name": "Cow",
"wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"
},
"to": {
"name": "Bob",
"wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"
},
"contents": "Hello, Bob!"
}
}
具体的,typed消息的签名理论可以参考:
普通的字符串消息msg
的签名为将消息加上前缀\x19Conflux Signed Message:\n
,再对其进行hash操作。
fluent签名
fluent钱包是一款为web3研发的简单而安全的conflux钱包,目前已支持的rpc功能可参考: https://conflux-chain.github.io/fluent-wallet-doc/docs/provider-rpc/
在安装了fluent钱包后,就可以去调用fluent的rpc功能。在浏览器页面进入https://fluent-wallet.zendesk.com/hc/zh-cn。此时,fluent钱包会连接上该网站。
在此页面打开console
,输入
> conflux
> conflux.isFluent
本章节将对如何用fluent对消息进行签名进行具体的说明。
personal_sign
在console
中输入以下命令
conflux
.request({
method: 'personal_sign',
params: [
'v0G9u7huK4mJb2K1', '<your_address>']})
此时页面会弹出来自fluent的签名消息
cfx_signTypedData_v4
在console
中输入以下命令
conflux
.request({
method: 'cfx_signTypedData_v4',
params: [
'<your_address>',
`{
"types": {
"CIP23Domain": [
{
"name": "name",
"type": "string"
},
{
"name": "version",
"type": "string"
},
{
"name": "chainId",
"type": "uint256"
},
{
"name": "verifyingContract",
"type": "address"
}
],
"Person": [
{
"name": "name",
"type": "string"
},
{
"name": "wallet",
"type": "address"
}
],
"Mail": [
{
"name": "from",
"type": "Person"
},
{
"name": "to",
"type": "Person"
},
{
"name": "contents",
"type": "string"
}
]
},
"primaryType": "Mail",
"domain": {
"name": "Ether Mail",
"version": "1",
"chainId": 1,
"verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
},
"message": {
"from": {
"name": "Cow",
"wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"
},
"to": {
"name": "Bob",
"wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"
},
"contents": "Hello, Bob!"
}
}`]
})
利用Java-conflux-sdk进行签名与验签
签名
java-conflux-sdk
支持了personal_sign
与cfx_signTypedData_v4
。具体的实现如下:
package conflux.web3j.crypto;
import org.junit.jupiter.api.Test;
import org.web3j.utils.Numeric;
import java.io.IOException;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class SignDataTests {
//cfx_signTypedData_v4
@Test
public void testSignValidStructure() throws IOException {
StructuredDataTests t = new StructuredDataTests();
//获取TypedData
String msg = t.getResource(
"build/resources/test/"
+ "structured_data_json_files/ValidStructuredData.json");
//根据msg创建编码器
StructuredDataEncoder dataEncoder = new StructuredDataEncoder(msg);
//将数据进行编码后得到msghash,再进行签名
org.web3j.crypto.Sign.SignatureData sign = org.web3j.crypto.Sign.signMessage(dataEncoder.hashStructuredData(), SampleKeys.KEY_PAIR, false);
assertEquals(
"0x371ef48d63082d3875fee13b392c5b6a7449aa638921cb9f3d419f5b6a817ba754d085965fb3a041c3b178d3ae3798ea322ae74cb687dd699b5f6045c7fe47a91c",
Numeric.toHexString(sign.getR()) + Numeric.toHexStringNoPrefix(sign.getS()) + Numeric.toHexStringNoPrefix(sign.getV()));
}
//personal_sign
@Test
public void testSignAnyMessage() throws IOException {
String message = "v0G9u7huK4mJb2K1";
//将msg加上前缀后进行编码后得到msghash,再进行签名
org.web3j.crypto.Sign.SignatureData sign = Sign.signPrefixedMessage(message.getBytes(), SampleKeys.KEY_PAIR);
assertEquals(
"0xbb0ee8492623f2ef6ed461ea638f8b5060b191a1c8830c93d84245f3fb27e20a755e24ff60fe76482dd4377a0aef036937ef88537b2d0fdd834a54e76ecafadc1c",
Numeric.toHexString(sign.getR()) + Numeric.toHexStringNoPrefix(sign.getS()) + Numeric.toHexStringNoPrefix(sign.getV()));
}
}
验签
sdk验签
java-conflux-sdk
为验签提供了ecrecover
方法与测试例程。具体的实现如下:
package conflux.web3j.crypto;
import org.junit.jupiter.api.Test;
import org.web3j.crypto.*;
import org.web3j.crypto.Sign;
import org.web3j.utils.Numeric;
import java.io.IOException;
import java.util.Arrays;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class ECRecoverTest {
private String getAddress() {
return Numeric.prependHexPrefix(Keys.getAddress(getPubKey()));
}
private String getPubKey() {
return SampleKeys.KEY_PAIR.getPublicKey().toString();
}
@Test
public void testSignAndRecoverMessage() {
String message = "v0G9u7huK4mJb2K1";
byte[] msgHash = conflux.web3j.crypto.Sign.getConfluxMessageHash(message.getBytes());
Sign.SignatureData sign = conflux.web3j.crypto.Sign.signPrefixedMessage(message.getBytes(), SampleKeys.KEY_PAIR);
//根据签名与地址进行recover,得到签名的地址
String recoverAddress = conflux.web3j.crypto.Sign.recoverSignature(sign, msgHash, getAddress());
assertEquals(recoverAddress, getAddress());
}
//在fluent中进行签名得到签名后,可以参考以下例程来进行recover
@Test
public void testRecoverTyped() throws IOException {
StructuredDataTests t = new StructuredDataTests();
String msg = t.getResource(
"build/resources/test/"
+ "structured_data_json_files/ValidStructuredData.json");
StructuredDataEncoder dataEncoder = new StructuredDataEncoder(msg);
//以下的签名可以根据上文中的fluent签名来获取
String signature =
"0x371ef48d63082d3875fee13b392c5b6a7449aa638921cb9f3d419f5b6a817ba754d085965fb3a041c3b178d3ae3798ea322ae74cb687dd699b5f6045c7fe47a91c";
//根据签名创建签名实例
byte[] signatureBytes = Numeric.hexStringToByteArray(signature);
byte v = signatureBytes[64];
if (v < 27) {
v += 27;
}
Sign.SignatureData sd =
new Sign.SignatureData(
v,
(byte[]) Arrays.copyOfRange(signatureBytes, 0, 32),
(byte[]) Arrays.copyOfRange(signatureBytes, 32, 64));
//getAddress()中的私钥应换为在fluent钱包中的账户私钥
String recoverAddress = conflux.web3j.crypto.Sign.recoverSignature(sd, dataEncoder.hashStructuredData(), getAddress());
assertEquals(recoverAddress, getAddress());
}
//以下实现与testSignAndRecoverMessage()一样,只是签名方式有区别
@Test
public void testSignAndRecoverTyped() throws IOException {
StructuredDataTests t = new StructuredDataTests();
String msg = t.getResource(
"build/resources/test/"
+ "structured_data_json_files/ValidStructuredData.json");
StructuredDataEncoder dataEncoder = new StructuredDataEncoder(msg);
Sign.SignatureData sign = Sign.signMessage(dataEncoder.hashStructuredData(), SampleKeys.KEY_PAIR, false);
String recoverAddress = conflux.web3j.crypto.Sign.recoverSignature(sign, dataEncoder.hashStructuredData(), getAddress());
assertEquals(recoverAddress, getAddress());
}
}
合约验签
合约验签的参考示例如下:
// file: CIP23DomainExample.sol
pragma solidity ^0.4.24;
contract Example {
struct Person {
string name;
address wallet;
}
struct Mail {
Person from;
Person to;
string contents;
}
bytes32 constant PERSON_TYPEHASH = keccak256(
"Person(string name,address wallet)"
);
bytes32 constant MAIL_TYPEHASH = keccak256(
"Mail(Person from,Person to,string contents)Person(string name,address wallet)"
);
struct CIP23Domain {
string name;
string version;
uint256 chainId;
}
struct VerifyClaim{
address userAddress;
uint256 randNo;
uint256 amount;
}
bytes32 constant CIP23DOMAIN_TYPEHASH = keccak256(
"CIP23Domain(string name,string version,uint256 chainId)"
);
bytes32 constant VERIFYCLAIM_TYPEHASH = keccak256(
"VerifyClaim(address userAddress,uint256 randNo,uint256 amount)"
);
bytes32 DOMAIN_SEPARATOR;
constructor () public {
DOMAIN_SEPARATOR = hash(CIP23Domain({
name: "VerifyClaim",
version: '1',
chainId: 97
}));
}
function hash(Person person) internal pure returns (bytes32) {
return keccak256(abi.encode(
PERSON_TYPEHASH,
keccak256(bytes(person.name)),
person.wallet
));
}
function hash(Mail mail) internal pure returns (bytes32) {
return keccak256(abi.encode(
MAIL_TYPEHASH,
hash(mail.from),
hash(mail.to),
keccak256(bytes(mail.contents))
));
}
function hash(CIP23Domain cip23Domain) internal pure returns (bytes32) {
return keccak256(abi.encode(
CIP23DOMAIN_TYPEHASH,
keccak256(bytes(cip23Domain.name)),
keccak256(bytes(cip23Domain.version)),
cip23Domain.chainId
));
}
function hash(VerifyClaim verifyclaim) internal pure returns (bytes32) {
return keccak256(abi.encode(
VERIFYCLAIM_TYPEHASH,
verifyclaim.userAddress,
verifyclaim.randNo,
verifyclaim.amount
));
}
function verify(VerifyClaim verifyclaim, uint8 v, bytes32 r, bytes32 s) internal view returns (bool) {
// Note: we need to use `encodePacked` here instead of `encode`.
bytes32 digest = keccak256(abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR,
hash(verifyclaim)
));
return ecrecover(digest, v, r, s) == 0x53dE6A872435F5286BEFd0b6fB3bC06742aF8C8F;
}
function test(address _userAddress, uint256 _randNO, uint256 _amount, uint8 _v, bytes32 _r, bytes32 _s) public view returns (bool) {
// Example signed message
VerifyClaim memory verifyclaim = VerifyClaim({
userAddress: _userAddress,
randNo: _randNO,
amount: _amount
});
assert(verify(verifyclaim, _v, _r, _s));
return true;
}
}
通过sdk部署该合约,再通过sdk去调用该合约。可以参考上述合约,在设计的合约当中加入验签功能,从而实现对操作是否真的由某用户进行的。
常见问题汇总
解决方案: 进入国内网站进行调用
解决方案:进入https://fluent-wallet.zendesk.com/hc/zh-cn, 再进行命令行调用。
No Comments