Skip to main content

Week7-8.15

Day1 CIP23与EIP712的异同

在用钱包为结构化的数据签名时,用户看到的只是一串处理后的hex字符串,无法得知具体签名的内容,这对信息、财产等安全造成了极大的威胁。为了解决该问题,以太坊引入了EIP712。与此相对的,conflux引入了CIP23来作为解决方案。CIP23与EIP712的差异总结如下:

  • 当签署一条非结构化的message时,conflux为message增加前缀\x19Conflux Signed Message:\n而不是以太坊中的\x19Ethereum Signed Message:\n

  • EIP712domain重命名为CIP23domain

  • 在CIP23domain中,chainId字段必须包含在其中。否则应该拒绝签署该结构化的数据。

Day2 如何基于CIP23签署消息

  • 当签名消息为非结构化消息时,为:

    encode(message : 𝔹⁸ⁿ) = "\x19Conflux Signed Message:\n" ‖ len(message) ‖ message where len(message) is the non-zero-padded ascii-decimal encoding of the number of bytes in message.

  • 当签名消息为结构化消息时,为:

    encode(domainSeparator : 𝔹²⁵⁶, message : 𝕊) = "\x19\x01" ‖ domainSeparator ‖ hashStruct(message) where domainSeparator and hashStruct(message) are defined below.

具体的,conflux-sdk为我们提供了相应的sdk简化签署消息的过程。以java-sdk为例:

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()));
    }
}

Day3 如何基于cip23去验证签名的真伪

基于cip23签署的消息有两种验证方式,分别是基于sdk与合约验签。

以java-sdk为例,基于sdk的验签方法为

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());
    }
}

这其中最主要的是recover()函数,我们来看看该函数的实现:

    public static String recoverSignature(SignatureData sd, byte[] data, String address) {
        String addressRecovered = null;

        // Iterate for each possible key to recover
        for (int i = 0; i < 4; i++) {
            BigInteger publicKey =
                    org.web3j.crypto.Sign.recoverFromSignature(
                            (byte) i,
                            new ECDSASignature(
                                    new BigInteger(1, sd.getR()), new BigInteger(1, sd.getS())),
                            data);

            if (publicKey != null) {
                addressRecovered =  Numeric.prependHexPrefix(Keys.getAddress(publicKey.toString()));

                if (addressRecovered.equals(address)) {
                    break;
                }
            }
        }

        return addressRecovered;
    }

以太坊的加密算法中提供了四种签名模式,在四种签名模式下,从签名中去还原公钥,再用公钥去得到地址,与目标地址进行匹配。具体的四种签名模式可参考https://github.com/web3j/web3j/blob/7dea3d99c5bdbfcc03aaeaa8575fb0c9a9a771ab/crypto/src/main/java/org/web3j/crypto/Sign.java#:~:text=public%20static%20BigInteger%20recoverFromSignature(int%20recId%2C%20ECDSASignature%20sig%2C%20byte%5B%5D%20message)%20%7B

对于合约验签,我们需要在合约当中去实现结构化数据类型,具体可见: https://wiki.conflux123.xyz/link/60#bkmrk-%E5%90%88%E7%BA%A6%E9%AA%8C%E7%AD%BE

Day4 如何用fluent钱包去签名

java-sdk的test例程中提供了两种方式去签名,一种是未知签名,用sdk签名后再用sdk进行recover,另一种是已知签名。那么如何得到这个已知签名呢?可以用fluent等方式来获得。具体的fluent钱包为我们提供了相当的provider rpc doc。fluent钱包不仅支持cfx下的签名,还支持eth环境下的签名。

为了实现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!"
    }
}`]
  })

弹出的消息框中有消息的message字段

在签署后可以得到同样的签名

Day5 目前支持cip23的sdk

目前java-sdk与js-sdk支持cip23,对应的例程分别在java-sdk cip23例程js-sdk cip23例程

后续go-sdk与python-sdk也会支持上cip23,敬请期待!