Skip to main content

PersonalMessage & CIP23

简介

conflux-sdk及fluent支持脱胎于以太坊EIP712CIP23CIP23的提出是为了提高签名的安全性。在支持CIP23前,若签名的消息是一串字符串,可以看到具体的内容,但若签名的消息是一个结构化的数据(typedData),则用户在钱包中看到的消息是一串处理过的哈希字串,没法看到具体的信息;

图1 支持EIP712前的metamask签名

在支持CIP23后,用户可以在钱包中看到自己签名的内容(如图1-2支持EIP712前后metamask的签名所示)。 图2 支持EIP712后的metamask签名

本文在此基础上介绍CIP23的基本概念,并以java-conflux-sdk为例,说明如何实现CIP23协议下的签名与验签。

基本概念

CIP23的基本概念与和EIP712之间的异同可以参考:https://github.com/Conflux-Chain/CIPs/blob/master/CIPs/cip-23.md

CIP23中主要用于签名的两个方法为personal_signcfx_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消息的签名理论可以参考:

https://github.com/Conflux-Chain/CIPs/blob/master/CIPs/cip-23.md#:~:text=include%20chainId%20field.-,Encoding%20method,-The%20set%20of

普通的字符串消息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钱包的js调用启动成功。

本章节将对如何用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!"
    }
}`]
  })

在该页面弹出来自fluent的签名消息

对其进行签名后得到签名

利用Java-conflux-sdk进行签名与验签

签名

java-conflux-sdk支持了personal_signcfx_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去调用该合约。可以参考上述合约,在设计的合约当中加入验签功能,从而实现对操作是否真的由某用户进行的。

常见问题汇总

  1. 在输入fluent命令行时,报了以下错误

解决方案: 进入国内网站进行调用

  1. 在输入fluent命令行时,报了以下错误 这是fluent钱包未连上网站所导致的。

解决方案:进入https://fluent-wallet.zendesk.com/hc/zh-cn, 再进行命令行调用。

参考代码

java-conflux-sdk-tests