Web3 Dapp 交易签名方式升级为EIP712标准的记录

2,185 阅读9分钟

一、背景

  1. Metamask 签名方法设计之初存在安全性漏洞
  2. personal_sign 方法通过为数据添加了一个前缀,使得无法随意模拟交易,然而在链上验证加了文本前缀的数据的成本很高)
  3. EIP712规范定义了最新的结构化数据签名方式 signTypedData_v4 。EIP712规范允许钱包以结构化和可读的格式在签名提示中显示数据。EIP712 在安全性和可用性方面向前迈出了一大步,因为用户将不再需要在难以理解的十六进制字符串上签名,这是一种可能令人困惑和不安全的做法。
  4. 项目合约进行EIP712协议升级,Dapp应用也要随之升级签名方式

二、MetaMask 目前支持的六种签名方法

  • eth_sign (开放式签名方法,可以对任何哈希签名,也可以对任何数据和交易签名,但是也因此容易被钓鱼网站攻击)
  • personal_sign(安全性较高,使用 UTF-8 编码更具可读性;验证数据成本很高)
  • signTypedData(同signTypedData_v1)
  • signTypedData_v1
  • signTypedData_v3(成本低廉、安全)
  • signTypedData_v4(成本低廉、安全、增加了对数组的支持、优化对结构的编码方式)

EIP712标准signTypedData系列签名主要的设计考虑:在链上验证便宜、用户可读,易理解、难以钓鱼签名

三、personal_sign签名方式

//web3.eth.personal.sign(dataToSign, address, password [, callback])
let msg ="Hello world"   // 待签名的数据
let address = "0x11f4d0A3c12e86B4b5F39B213F7E19D048276DAe"  // 用于签署数据的签名地址
let password = "test password"  // 用于签署数据的帐户的密码。
web3.eth.personal.sign(msg, address, password).then((res) => {
  console.log("signRes===", res)
});
// 或者
web3.eth.personal.sign(msg, address, password,(err,res) => {
  console.log("sign data result======", res)
})

四、signTypedData_v4签名方式(根据RIP-712标准生成签名)

(1)参数定义说明

  • domain:域分隔符。为必填字段,有助于防止用于一个 dApp 的签名在另一个 dApp 中工作。
  • name : dApp 或协议名称,例如“CryptoKitties”
  • version:“签名域”的当前版本。也可以是 dApp 或平台的版本号。它可以防止来自一个 dApp 版本的签名与其他版本的签名一起使用。
  • chainId:链 ID。防止用于一个网络(例如测试网)的签名在另一个网络(例如主网)上工作。如果钱包提供商与其当前连接的网络不匹配,将无法进行签名
  • verifyingContract:将验证结果签名的合约的以太坊地址
  • **message: **消息数据,根据合约自定义的任意格式的JSON数据
  • primaryType:指明匹配哪个types对象的键值。
  • types:定义消息要用的结构

(1)signTypedData_v4 签名示例

      const msgParams = JSON.stringify({
        domain: {
          chainId: 4,
          name: '1inch Limit Order Protocol',
          verifyingContract: '0x798D45649FD9b578133D9486d006325214e98c81',
          version: '2',
        },
        message: {
          salt: "1663577180488",
          makerAsset: "0x6701069b74ed20aA631d9D53C6ff2b2198bCCE56",
          takerAsset: "0xE84f4F2Dde53bd3e6D2e74babcF5380D85dae3f9",
          maker: "0x17a99B62Eb6Db79D2b791eA895Dd61A404074C39",
          receiver: "0x0000000000000000000000000000000000000000",
          allowedSender: "0x0000000000000000000000000000000000000000",
          makingAmount: "1000000000000000",
          takingAmount: "2000000000000000",
          makerAssetData: "0x",
          takerAssetData: "0x",
          getMakerAmount: "0xf4a215c300000000000000000000000000000000000000000000000000038d7ea4c6800000000000000000000000000000000000000000000000000000071afd498d0000",
          getTakerAmount: "0x296637bf00000000000000000000000000000000000000000000000000038d7ea4c6800000000000000000000000000000000000000000000000000000071afd498d0000",
          predicate: "0x961d5b1e000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000002000000000000000000000000798d45649fd9b578133d9486d006325214e98c81000000000000000000000000798d45649fd9b578133d9486d006325214e98c810000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000044cf6fc6e300000000000000000000000017a99b62eb6db79d2b791ea895dd61a404074c39000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002463592c2b0000000000000000000000000000000000000000000000000000000063283a6c00000000000000000000000000000000000000000000000000000000",
          permit: "0x",
          interaction: "0x"
        },
        // Refers to the keys of the *types* object below.
        primaryType: 'Order',
        types: {
          // TODO: Clarify if EIP712Domain refers to the domain the contract is hosted on
          EIP712Domain: [
            { name: 'name', type: 'string' },
            { name: 'version', type: 'string' },
            { name: 'chainId', type: 'uint256' },
            { name: 'verifyingContract', type: 'address' },
          ],
          // Not an EIP712Domain definition
          Order: [
            { name: "salt", type: "uint256" },
            { name: "makerAsset", type: "address" },
            { name: "takerAsset", type: "address" },
            { name: "maker", type: "address" },
            { name: "receiver", type: "address" },
            { name: "allowedSender", type: "address" },
            { name: "makingAmount", type: "uint256" },
            { name: "takingAmount", type: "uint256" },
            { name: "makerAssetData", type: "bytes" },
            { name: "takerAssetData", type: "bytes" },
            { name: "getMakerAmount", type: "bytes" },
            { name: "getTakerAmount", type: "bytes" },
            { name: "predicate", type: "bytes" },
            { name: "permit", type: "bytes" },
            { name: "interaction", type: "bytes" }
          ],
        },
      });

      var from = await web3.eth.getAccounts();

      console.log("from====>", from)
      var params = [from[0], msgParams];
      var method = 'eth_signTypedData_v4';

      web3.currentProvider.sendAsync(
        {
          method,
          params,
          from: from[0],
        },
        function (err, result) {
          if (err) return console.dir(err);
          if (result.error) {
            alert(result.error.message);
          }
          if (result.error) return console.error('ERROR', result);
          console.log('TYPED SIGNED:' + JSON.stringify(result.result));

          const recovered = sigUtil.recoverTypedSignature_v4({
            data: JSON.parse(msgParams),
            sig: result.result,
          });

          if (
            ethUtil.toChecksumAddress(recovered) === ethUtil.toChecksumAddress(from)
          ) {
            alert('Successfully recovered signer as ' + from);
          } else {
            alert(
              'Failed to verify signer when comparing ' + result + ' to ' + from
            );
          }
        }
      );
    

(2)signTypedData_v4 实际应用示例

import { ExchangeAddress } from "@utils/contractUtil/index";
let PRO_ENVIRONMENT = process.env.REACT_APP_ENVIRONMENT === "production";
const createSignMessage = (messageReq, domainReq, typesReq, primaryTypeReq) => {
	// domain 域分隔符(签名域) 有助于防止用于一个 dApp 的签名在另一个 dApp 中工作
	let domain = domainReq || {
		chainId: PRO_ENVIRONMENT ? 56 : 4, // chainId 告诉你你在哪个链上,它确保在 Rinkeby 上签名的签名在另一条链上无效,例如以太坊主网。
		name: "Test Exchange Contract", //  dApp 或协议名称(交易合约名字)
		verifyingContract: ExchangeAddress, // 将验证结果签名的合约的以太坊地址
		version: "2.3" //  签名域的当前版本
	};
	let primaryType = primaryTypeReq || "Order"; // 指明匹配types对象的键值。
	let types = typesReq || {
		EIP712Domain: [
			{ name: "name", type: "string" },
			{ name: "version", type: "string" },
			{ name: "chainId", type: "uint256" },
			{ name: "verifyingContract", type: "address" }
		],
		Order: [
			{ name: "exchange", type: "address" },
			{ name: "maker", type: "address" },
			{ name: "taker", type: "address" },
			{ name: "makerRelayerFee", type: "uint256" },
			{ name: "takerRelayerFee", type: "uint256" },
			{ name: "makerProtocolFee", type: "uint256" },
			{ name: "takerProtocolFee", type: "uint256" },
			{ name: "feeRecipient", type: "address" },
			{ name: "feeMethod", type: "uint8" },
			{ name: "side", type: "uint8" },
			{ name: "saleKind", type: "uint8" },
			{ name: "target", type: "address" },
			{ name: "howToCall", type: "uint8" },
			{ name: "calldata", type: "bytes" },
			{ name: "replacementPattern", type: "bytes" },
			{ name: "staticTarget", type: "address" },
			{ name: "staticExtradata", type: "bytes" },
			{ name: "paymentToken", type: "address" },
			{ name: "basePrice", type: "uint256" },
			{ name: "extra", type: "uint256" },
			{ name: "listingTime", type: "uint256" },
			{ name: "expirationTime", type: "uint256" },
			{ name: "salt", type: "uint256" },
			{ name: "nonce", type: "uint256" }
		]
	};
	let message = messageReq;
	return JSON.stringify({ types, primaryType, domain, message });
};

// SignTypedData_V4 版本签名
const typedDataSignV4 = async (web3, message, from) => {
	return new Promise((resolve, reject) => {
		let msgParams = createSignMessage(message);
		let params = [from, msgParams];
		let method = "eth_signTypedData_v4";
		console.log("msgParams===============", JSON.parse(msgParams));
		web3.currentProvider.sendAsync(
			{
				method,
				params,
				from: from // 地址
			},
			function (err, result) {
				if (err) {
					console.log("err", err);
					toast.error(err.message);
					reject(false);
				}
				// if (result.error) {
				// 	toast.error(result.error.message);
				// 	reject(err);
				// }
				if (result.error) return console.error("ERROR", result);
				console.log("TYPED SIGNED:" + JSON.stringify(result.result));
				let signData = JSON.stringify(result.result);
				resolve(signData);
			}
		);
	});
};

(2)小狐狸唤起签名对比

image.pngimage.png

五、验证类型化的数据签名

**借助 eth-sig-util方法库验证signTypedData_v4签名数据 **
**1、**npm install @metamask/eth-sig-util

import { SignTypedDataVersion, recoverTypedSignature } from "@metamask/eth-sig-util";

const recovered = recoverTypedSignature({
        data: JSON.parse(msgParams), // msgParams 为签名方法的参数msgParams
        sig: result.result,  // typedDataSignV4签名结果
        version: SignTypedDataVersion.V4
});
console.log("recovered==========",recovered)