一、背景
- Metamask 签名方法设计之初存在安全性漏洞
- personal_sign 方法通过为数据添加了一个前缀,使得无法随意模拟交易,然而在链上验证加了文本前缀的数据的成本很高)
- EIP712规范定义了最新的结构化数据签名方式 signTypedData_v4 。EIP712规范允许钱包以结构化和可读的格式在签名提示中显示数据。EIP712 在安全性和可用性方面向前迈出了一大步,因为用户将不再需要在难以理解的十六进制字符串上签名,这是一种可能令人困惑和不安全的做法。
- 项目合约进行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)小狐狸唤起签名对比
五、验证类型化的数据签名
**借助 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)