前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >应用EIP712

应用EIP712

作者头像
Tiny熊
发布2022-04-11 13:44:00
2K0
发布2022-04-11 13:44:00
举报
文章被收录于专栏:深入浅出区块链技术

本文作者:影无双[1]

以太坊钱包如MetaMask[2]都支持EIP712[3] —— 类型结构化消息签名[4]标准,让钱包可以结构化和可读的格式在签名提示中显示数据。EIP712 在安全性和可用性方面向前迈进了一大步,因为用户不再需要对难以理解的十六进制字符串签名(这是一种令人困惑、不安全的做法)。

EIP712 已合并到以太坊改进提案库[5],主流钱包也已支持。本文旨在帮助开发者应用它,包括对其功能的描述、示例 JavaScript 和 Solidity 代码,以及演示。

EIP712 之前

- 图 1: 不使用 EIP712 的 dApp 的签名请求 -

加密货币领域的格言是:不信任;验证。然而,在 EIP712 之前,用户很难验证被要求签名的数据,在以签名信息作为后续交易基础的 DApp 中,很容易给予更多的信任。

例如,图 1 是一个由去中心化交易触发的 MetaMask 弹窗,为了安全地将与钱包地址关联起来,要求用户对订单的哈希值进行签名。不幸的是,由于这个哈希值是一个十六进制字符串,没有专业技术知识的用户无法轻松地验证这个哈希值。对于普通用户来说,更容易盲目地相信 DApp 并点击“签名”,而不是通过麻烦的技术验证。这不利于安全。

如果用户无意中登陆了一个恶意的网络钓鱼 DApp,就可能会签下错误的订单信息。例如,可以欺骗用户,让他们为一笔本来成本较低的交易支付不合理的高额以太币。为了防止此类攻击,用户必须通过某种方式确切地知道所签名的内容,而不必自己费力地重新构哈希。

EIP712 的改进

- 图 2: 使用 EIP712 的 DApp 的签名请求 -

EIP712 在可用性和安全性方面有很大的改进。与上面的例子相反,当启用 EIP712 的 DApp 请求签名时,用户的钱包会显示哈希之前的原始数据,这样用户更容易验证它。

如何实现 EIP712

标准引入了几个开发人员必须熟悉的概念,本节将详细介绍如何在 DApp 中实现它。

举个例子,你正在构建一个去中心化的拍卖 DApp,在这个 DApp 中,竞标者在链下签名竞价,一个智能合约会在链上验证这些已经签名的竞价。

1、设计数据结构

首先,设计你希望用户签名的数据的 JSON 结构。本例中我们如下设计:

代码语言:javascript
复制
{
    amount: 100,
    token: “0x….”,
    id: 15,
    bidder: {
        userId: 323,
        wallet: “0x….”
    }
}

然后,我们可以从上面的代码片段中派生出两个数据结构:Bid,它包括以 ERC20 代币计价的出价amount和拍卖id,以及Identity,它指定了一个userIdwallet地址。

接下来,写下BidIdentity作为你会在你的 solididity 代码中使用结构体。参考EIP712 标准[6]获取完整的本地数据类型列表,如address, bytes32, uint256等。

代码语言:javascript
复制
Bid: {
    amount: uint256,
    bidder: Identity
}
Identity: {
    userId: uint256,
    wallet: address
}

2、设计域分隔符

下一步是创建一个域分隔符。这个强制字段有助于防止一个 DApp 的签名被用在另一个 DApp 中。如 EIP712 的说明[7]:

两个 DApp 可能会出现相同的结构,如Transfer(address from,address to,uint256 amount),这应该是不兼容的。通过引入域分隔符,DApp 开发人员可以保证不会出现签名冲突。

域分隔符需要在体系结构和实现级别上进行仔细的思考和努力。开发人员和设计人员必须根据对他们的用例有意义的内容来决定要包含或排除哪个字段。

name: DApp 或协议的名称,如“CryptoKitties”

version: “签名域”的当前版本。可以是你的 DApp 或平台的版本号。它阻止一个 DApp 版本的签名与其他 DApp 版本的签名一起工作。

chainId: EIP-155[8]链 id。防止一个网络(如测试网)的签名在另一个网络(如主网)上工作。

verifyingContract: 将要验证签名的合约的以太坊地址。Solidity 中的this关键字返回合约自己的地址,可以在验证签名时使用。

salt: 在合约和 DApp 中都硬编码的惟一的 32 字节值,这是将 DApp 与其他应用区分开来的最后手段。

应用上面所有的域分隔符,如下:

代码语言:javascript
复制
{
    name: "My amazing dApp",
    version: "2",
    chainId: "1",
    verifyingContract: "0x1c56346cd2a2bf3202f771f50d3d14a367b48070",
    salt: "0x43efba6b4ccb1b6faa2625fe562bdd9a23260359"
}

关于chainId需要注意的一点是,如果它与当前连接的网络不匹配,钱包应该阻止签名。然而,由于钱包不一定强制执行这一点,关键是要在链上验证chainId。唯一需要注意的是,合约没有办法找到它们所在的链 ID,所以开发者必须将chainId硬编码到他们的合约中,并且要格外小心,确保它与部署的网络相对应。

写于(2019 年 5 月 31 日):如果EIP-1344[9]被包含在未来的以太坊升级中(可能是伊斯坦布尔[10]),将会有一种方法让合约通过编程方式找到chainId

2.1、安装 4.14.0 或以上版本的 MetaMask

在 4.14.0 版本之前的 MetaMask,由于 ETHSanFrancisco 的回滚,对 EIP712 的支持略有变化。4.14.0 和更高版本可以正确支持 EIP712 签名。

3、为 DApp 编写签名代码

您的 JavaScript DApp 现在需要能够要求 MetaMask 为数据签名。首先,定义数据类型:

代码语言:javascript
复制
const domain = [
    { name: "name", type: "string" },
    { name: "version", type: "string" },
    { name: "chainId", type: "uint256" },
    { name: "verifyingContract", type: "address" },
    { name: "salt", type: "bytes32" },
];
const bid = [
    { name: "amount", type: "uint256" },
    { name: "bidder", type: "Identity" },
];
const identity = [
    { name: "userId", type: "uint256" },
    { name: "wallet", type: "address" },
];

接下来,定义域分隔符和消息数据。

代码语言:javascript
复制
const domainData = {
    name: "My amazing dApp",
    version: "2",
    chainId: parseInt(web3.version.network, 10),
    verifyingContract: "0x1C56346CD2A2Bf3202F771f50d3D14a367B48070",
    salt: "0xf2d857f4a3edcb9b78b4d503bfe733db1e3f6cdc2b7971ee739626c97e86a558"
};
var message = {
    amount: 100,
    bidder: {
        userId: 323,
        wallet: "0x3333333333333333333333333333333333333333"
    }
};

变量:

代码语言:javascript
复制
const data = JSON.stringify({
    types: {
        EIP712Domain: domain,
        Bid: bid,
        Identity: identity,
    },
    domain: domainData,
    primaryType: "Bid",
    message: message
});

接下来,让eth_signTypedData_v3签名调用web3:

代码语言:javascript
复制
web3.currentProvider.sendAsync(
{
    method: "eth_signTypedData_v3",
    params: [signer, data],
    from: signer
},
function(err, result) {
    if (err) {
        return console.error(err);
    }
    const signature = result.result.substring(2);
    const r = "0x" + signature.substring(0, 64);
    const s = "0x" + signature.substring(64, 128);
    const v = parseInt(signature.substring(128, 130), 16);
    // The signature is now comprised of r, s, and v.
    }
);

请注意,在撰写本文时,MetaMask 和 Cipher Browser 在 method 字段中使用eth_signTypedData_v3,以便向后兼容,DApp 生态系统就采用这个标准。这些钱包的未来版本可能会将其重命名为eth_signTypedData

4、为验证的合约编写身份验证代码

回想一下,在钱包签名 EIP712 类型数据之前,它会先对数据进行格式化和哈希处理。你的合约需要能够做同样的事情,以便用ecrecover来确定是哪个地址签名的,你需要在 Solidity 合约代码中复制这个格式化/哈希函数。这可能是最棘手的一步,所以要非常小心。

首先,在 Solidity 中声明数据类型,你应该已经在前面做了:

代码语言:javascript
复制
struct Identity {
    uint256 userId;
    address wallet;
}
struct Bid {
    uint256 amount;
    Identity bidder;
}

接下来,定义适合你的数据结构的类型哈希。注意,逗号和方括号后面没有空格,并且名称和类型应该与上面 JavaScript 代码中指定的名称和类型完全匹配。

代码语言:javascript
复制
string private constant IDENTITY_TYPE = "Identity(uint256 userId,address wallet)";
string private constant BID_TYPE = "Bid(uint256 amount,Identity bidder)Identity(uint256 userId,address wallet)";

还要定义域分隔符类型哈希。请注意,下面的chainId为 1 表示合约要部署到主网,并且字符串(如“My amazing dApp”)必须被哈希。

代码语言:javascript
复制
uint256 constant chainId = 1;
address constant verifyingContract = 0x1C56346CD2A2Bf3202F771f50d3D14a367B48070;
bytes32 constant salt = 0xf2d857f4a3edcb9b78b4d503bfe733db1e3f6cdc2b7971ee739626c97e86a558;
string private constant EIP712_DOMAIN = "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)";
bytes32 private constant DOMAIN_SEPARATOR = keccak256(abi.encode(
    EIP712_DOMAIN_TYPEHASH,
    keccak256("My amazing dApp"),
    keccak256("2"),
    chainId,
    verifyingContract,
    salt
));

接下来,为每种数据类型写一个哈希函数:

代码语言:javascript
复制
function hashIdentity(Identity identity) private pure returns (bytes32) {
    return keccak256(abi.encode(
        IDENTITY_TYPEHASH,
        identity.userId,
        identity.wallet
    ));
}
function hashBid(Bid memory bid) private pure returns (bytes32){
    return keccak256(abi.encodePacked(
        "\\x19\\x01",
       DOMAIN_SEPARATOR,
       keccak256(abi.encode(
            BID_TYPEHASH,
            bid.amount,
            hashIdentity(bid.bidder)
        ))
    ));

最后,同样重要的是,编写签名验证函数:

代码语言:javascript
复制
function verify(address signer, Bid memory bid, sigR, sigS, sigV) public pure returns (bool) {
    return signer == ecrecover(hashBid(bid), sigV, sigR, sigS);
}

演示

要演示上述代码,请使用此工具[11]。安装与 EIP712 兼容的 MetaMask 版本后,单击页面上的按钮以运行 JavaScript 代码来触发一个签名请求。点击 Sign,solididity 代码将出现在一个文本框。

此代码包含上述所有哈希代码、MetaMask 生成的签名、你的钱包地址。如果你将它复制粘贴到 Remix IDE[12],选择 JavaScript VM 环境,然后运行verify功能,Remix 将在代码中运行ecrecover获取签名者的地址,将结果与钱包地址比较,如果匹配则返回true

请注意,为了简单起见,演示生成的verify函数与上面给出的示例不同,因为由 MetaMask 生成的签名会动态地插入其中。

- 图 3: 运行验证函数时 Remix 显示的内容 -

实际上,这就是智能合约验证签名数据应该做的。您可以根据自己的需要调整代码。希望可以在给数据结构写哈希函数时节省时间。

MetaMask 支持 EIP712 后关于“legacy” 的说明

另一件需要注意的事情是,当 MetaMask 发布对 EIP712 支持时,它将不再支持一个实验性的“legacy”类型化数据签名,正如这篇2017 年 10 月的博客文章[13]所描述的。

写于(9 月 29 日):据我理解,一旦 MetaMask 让eth_signTypedData指向完整的 EIP712 支持,它将通过eth_signTypedData_v1调用支持 legacy 类型化数据签名。

最后

总之,开发人员应该充分利用 EIP712。它显著提高了可用性,并有助于防止网络钓鱼,希望本文能够帮助开发人员在自己的 DApp 和合约中应用它。

特别感谢

本文作者 Koh Wei Jie 曾是 ConsenSys Singapore 的一名全栈开发人员。非常感谢 Paul Bouchon 和 Dan Finlay 的宝贵反馈和评论。

原文链接:https://medium.com/metamask/eip712-is-coming-what-to-expect-and-how-to-use-it-bb92fd1a7a26

参考资料

[1]

影无双: https://learnblockchain.cn/people/58

[2]

MetaMask: https://metamask.io/

[3]

EIP712: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md

[4]

类型结构化消息签名: https://learnblockchain.cn/2019/04/24/token-EIP712

[5]

以太坊改进提案库: https://github.com/ethereum/EIPs

[6]

EIP712标准: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md#definition-of-typed-structured-data-%F0%9D%95%8A

[7]

说明: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md#rationale

[8]

EIP-155: https://eips.ethereum.org/EIPS/eip-155

[9]

EIP-1344: https://eips.ethereum.org/EIPS/eip-1344

[10]

伊斯坦布尔: http://eips.ethereum.org/EIPS/eip-1679

[11]

此工具: https://weijiekoh.github.io/eip712-signing-demo/index.html

[12]

Remix IDE: https://remix.ethereum.org/#optimize=true&version=soljson-v0.4.24+commit.e67f0147.js

[13]

2017年10月的博客文章: https://medium.com/metamask/scaling-web3-with-signtypeddata-91d6efc8b290

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-03-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 深入浅出区块链技术 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • EIP712 之前
  • EIP712 的改进
  • 如何实现 EIP712
    • 1、设计数据结构
      • 2、设计域分隔符
        • 2.1、安装 4.14.0 或以上版本的 MetaMask
      • 3、为 DApp 编写签名代码
        • 4、为验证的合约编写身份验证代码
        • 演示
        • MetaMask 支持 EIP712 后关于“legacy” 的说明
        • 最后
        • 特别感谢
          • 参考资料
          相关产品与服务
          多因子身份认证
          多因子身份认证(Multi-factor Authentication Service,MFAS)的目的是建立一个多层次的防御体系,通过结合两种或三种认证因子(基于记忆的/基于持有物的/基于生物特征的认证因子)验证访问者的身份,使系统或资源更加安全。攻击者即使破解单一因子(如口令、人脸),应用的安全依然可以得到保障。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档