Alert Source Discuss
⚠️ Draft Standards Track: ERC

ERC-7754: 防篡改 Web 不可变交易 (TWIT)

提供了一种机制,使 dapps 能够以防篡改的方式使用扩展钱包 API

Authors Erik Marks (@remarks), Guillaume Grosbois (@uni-guillaume)
Created 2024-07-29
Discussion Link https://ethereum-magicians.org/t/erc-7754-tamperproof-web-immutable-transaction-twit/20767
Requires EIP-1193

摘要

引入了一种新的 RPC 方法 wallet_signedRequest,该方法将由钱包实现, 使 dapps 能够通过“签名请求”以防篡改的方式与钱包交互。 Dapp 将公钥与其 DNS 记录关联,并使用相应的私钥对通过 wallet_signedRequest 发送到钱包的 payload 进行签名。 然后,钱包可以使用 DNS 记录中的公钥来验证 payload 的完整性。

动机

本标准旨在通过使用户确信来自其 dapps 的请求未被篡改来增强最终用户的体验。 本质上,这类似于 HTTPS 在网络中的使用方式。

目前,dapps 和钱包之间的通信通道容易受到中间人攻击。 具体来说,攻击者可以通过在页面中注入 JavaScript 代码来拦截 RPC 请求, 例如,通过 XSS 漏洞或由于恶意扩展。 一旦 RPC 请求被拦截,就可以通过多种有害方式对其进行修改,包括:

  • 编辑 calldata 以盗取资金或以其他方式更改交易结果
  • 修改 EIP-712 请求的参数
  • 从钱包获取可重放的签名

即使使用者意识到来自 dapp 的请求可能被篡改,他们也几乎没有追索权来缓解问题。 总的来说,dapp 和钱包之间缺乏信任链会损害整个生态系统:

  • 用户不能简单地信任原本诚实的 dapps,并且有丢失资金的风险
  • 如果攻击者发现可行的 MITM 攻击,Dapp 维护者可能会损害其声誉

由于这些原因,我们建议钱包实现 wallet_signedRequest RPC 方法。 此方法为 dapp 开发者提供了一种显式要求钱包验证 payload 完整性的方法。 这比强制 dapps 依赖诸如参数位打包之类的隐式方法的状态有了显着改进。

规范

本文档中的关键词“必须”、“不得”、“必需”、“应”、“不应”、“应该”、“不应该”、“推荐”、“不推荐”、“可以”和“可选”应按照 RFC 2119 和 RFC 8174 中的描述进行解释。

概述

我们建议使用 dapp 的域证书作为信任根,以建立信任链,如下所示:

  1. 用户的浏览器验证域证书,并在被接管时显示适当的警告
  2. Dapp 的 DNS 记录托管一个 TXT 字段,该字段指向托管 JSON manifest 的 URL
    • 此文件 SHOULD 位于众所周知的地址,例如 https://example.com/.well-known/twit.json
  3. 配置文件包含 ` { id, alg, publicKey }` 形式的对象数组
  4. 对于签名请求,dapp 首先使用私钥安全地对 payload 进行签名,例如通过向其后端提交请求
  5. 原始 payload、签名和公钥 id 通过 wallet_signedRequest RPC 方法发送到钱包
  6. 钱包在正常处理请求之前验证签名

钱包集成

密钥发现

为了建立信任链,经过认证的公钥是必不可少的。 由于传统上这是通过 DNS 证书完成的,因此我们建议添加一个包含公钥的 DNS 记录。 这类似于 RFC-6636 的 DKIM,但是 manifest 文件的使用为未来的改进提供了更大的灵活性,并支持多种算法和密钥对。

与标准 RFC-7519 的 JWT 实践类似,钱包可以主动缓存 dapp 密钥。 但是,在没有撤销机制的情况下,受损的密钥仍可以使用,直到缓存过期。 为了缓解这种情况,钱包不 SHOULD 缓存 dapp 公钥超过 2 小时。 这种做法建立了一个相对较短的漏洞窗口,并为钱包和 dapp 维护者带来了可管理的开销。

my-crypto-dapp.invalid 的示例 DNS 记录:

...
TXT: TWIT=/.well-known/twit.json

https://my-crypto-dapp.invalid.com/twit.json 上的示例 TWIT manifest:

{
  "publicKeys": [
    { "id": "1", "alg": "ECDSA", "publicKey": "0xaf34..." },
    { "id": "2", "alg": "RSA-PSS", "publicKey": "0x98ab..." }
  ]
}

Dapps SHOULD 仅依赖于通过 SubtleCrypto 提供的算法,因为它们存在于每个浏览器中。

Manifest 模式

我们提出了一个简单且可扩展的模式:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "TWIT manifest",
  "type": "object",
  "properties": {
    "publicKeys": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "alg": { "type": "string" },
          "publicKey": { "type": "string" }
        }
      }
    }
  }
}

RPC 方法

wallet_signedRequest 的参数由此 TypeScript 接口指定:

type RequestPayload<Params> = { method: string; params: Params };

type SignedRequestParameters<Params> = [
  requestPayload: RequestPayload<Params>,
  signature: `0x${string}`,
  keyId: string,
];

这是一个使用 EIP-1193 提供程序接口调用 wallet_signedRequest 的非规范示例:

const keyId = '1';
const requestPayload: RequestPayload<TransactionParams> = {
  method: 'eth_sendTransaction',
  params: [
    {
      /* ... */
    },
  ],
};
const signature: `0x${string}` = await getSignature(requestPayload, keyId);

// Using the EIP-1193 provider interface
const result = await ethereum.request({
  method: 'wallet_signedRequest',
  params: [requestPayload, signature, keyId],
});

签名验证

  1. 收到 EIP-1193 调用后,钱包 MUST 检查 sender.tab.url 域的 TWIT manifest 是否存在 a. 钱包 MUST 验证 manifest 是否托管在 sender.tab.url 域上 b. 钱包 SHOULD 查找 DNS TXT 记录以找到 manifest 位置 b. 钱包 MAY 首先尝试 /.well-known/twit.json 位置
  2. 如果未为 sender.tab.url 域配置 TWIT,则照常进行
  3. 如果配置了 TWIT 且使用了 request 方法,则钱包 SHOULD 向用户显示可见且可操作的警告 a. 如果用户选择忽略警告,则照常进行 b. 如果用户选择取消,则钱包 MUST 取消调用
  4. 如果配置了 TWIT 并且使用了 wallet_signedRequest 方法,参数为 requestPayloadsignaturekeyId,则: a. 钱包 MAY 显示可见的提示,表明此交互已签名 b. 钱包 MUST 验证 keyId 是否存在于 TWIT manifest 中,并找到关联的密钥记录 c. 从密钥记录中,钱包 MUST 使用 alg 字段和 publicKey 字段,通过调用 crypto.verify(alg, key, signature, requestPayload) 来验证 requestPayload 的完整性 d. 如果签名无效,钱包 MUST 向用户显示可见且可操作的警告 i. 如果用户选择忽略警告,则继续使用参数 requestPayload 调用 request ii. 如果用户选择取消,则钱包 MUST 取消调用 e. 如果签名有效,钱包 MUST 使用参数 requestPayload 调用 request

示例方法实现(钱包)

async function signedRequest(
  requestPayload: RequestPayload<unknown>,
  signature: `0x${string}`,
  keyId: string,
): Promise<unknown> {
  // 1. Get the domain of the sender.tab.url
  // 1. 获取 sender.tab.url 的域
  const domain = getDappDomain();

  // 2. Get the manifest for the current domain
  // 2. 获取当前域的 manifest
  // It's possible to use RFC 8484 for the actual DNS-over-HTTPS specification, see https://datatracker.ietf.org/doc/html/rfc8484.
  // 可以使用 RFC 8484 作为实际的基于 HTTPS 的 DNS 规范,请参阅 https://datatracker.ietf.org/doc/html/rfc8484。
  // However, here we are doing it with DoHjs.
  // 但是,这里我们使用 DoHjs 来实现。
  // This step is optional, and you could go directly to the well-known address first at `domain + '/.well-known/twit.json'`
  // 此步骤是可选的,您可以首先直接转到众所周知的地址 `domain + '/.well-known/twit.json'`
  const doh = require('dohjs');
  const resolver = new doh.DohResolver('https://1.1.1.1/dns-query');

  let manifestPath = '';
  const dnsResp = await resolver.query(domain, 'TXT');
  for (record of dnsResp.answers) {
    if (!record.data.startsWith('TWIT=')) continue;

    manifestPath = record.data.substring(5); // This should be domain + '/.well-known/twit.json'
    // 这应该是 domain + '/.well-known/twit.json'
    break;
  }

  // 3. Parse the manifest and get the key and algo based on `keyId`
  // 3. 解析 manifest 并根据 `keyId` 获取密钥和算法
  const manifestReq = await fetch(manifestPath);
  const manifest = await manifestReq.json();
  const keyData = manifest.publicKeys.filter((x) => x.id == keyId);
  if (!keyData) {
    throw new Error('Could not find the signing key');
    // 找不到签名密钥
  }

  const key = keyData.publicKey;
  const alg = keyData.alg;

  // 4. Verify the signature
  // 4. 验证签名
  const valid = await crypto.verify(alg, key, signature, requestPayload);
  if (!valid) {
    throw new Error('The data was tampered with');
    // 数据已被篡改
  }
  return await processRequest(requestPayload);
}

钱包用户体验建议

与 HTTPS 的挂锁图标类似,当在域上配置了 TWIT 时,钱包应显示可见的指示。 这将改善最终用户的体验,最终用户将能够立即知道他们正在使用的 dapp 和钱包之间的交互是安全的,并且这将鼓励 dapp 开发者采用 TWIT,从而使整个生态系统更加安全

当处理不安全的请求时,无论是由于 dapp(或攻击者)在配置了 TWIT 的域上使用 request,还是因为签名不匹配,钱包都应该警告用户,但不要阻止:措辞精炼的警告将提高透明度,最终用户可以选择取消交互或继续执行不安全调用。

理由

拟议的实现不会修改 EIP-712EIP-1193 提供的任何现有功能。 它的添加性质使其本质上向后兼容。 它的核心设计是模仿现有解决方案来解决现有问题(例如 DKIM)。 因此,对于钱包和 dapps 而言,拟议的规范将是非破坏性的,易于实现,并且具有可预测的威胁模型。

安全考虑

防止重放

虽然对 requestArg payload 进行签名可以保证数据完整性,但它本身并不能防止重放攻击:

  1. 签名的 payload 可以多次重放
  2. 签名的 payload 可以在多个链上重放

1. 中所述,有效的时间重放攻击通常通过交易 nonce 来防止。 可以通过利用 EIP-712 signTypedData 方法来防止跨链重放。

在任何不受保护的方法上仍然可能发生重放攻击:这实际上影响了所有对于攻击者而言价值非常有限的“只读”方法。

由于这些原因,我们目前不建议使用特定的重放保护机制。 如果/当需要时,manifest 的可扩展性将为受影响的 dapp 强制执行重放保护信封(例如:JWT)提供必要的空间。

版权

CC0 下放弃版权及相关权利。

Citation

Please cite this document as:

Erik Marks (@remarks), Guillaume Grosbois (@uni-guillaume), "ERC-7754: 防篡改 Web 不可变交易 (TWIT) [DRAFT]," Ethereum Improvement Proposals, no. 7754, July 2024. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7754.