可信签名器
Vela 在你批准每一笔交易之前都会先解码,这份解码是实打实的活——但干这活的,是构造这笔交易的同一个应用。如果这个应用、或者它送到你手上的那条路被动了手脚,它就能给你看一样东西、却签下另一样。Bybit 遇到的正是这件事。
可信签名器存在的意义,就是把这件事一分为二:App 只负责把请求交出去,核对和签名则发生在另一个页面上——一个你可以从头读到尾、逐字节重新构建、或者自己运行的页面。
它在哪里运行
官方页面由 sign.getvela.app 提供。桌面版(macOS、Windows、Linux)、iPhone 和 Android App 都可以把请求发给它:App 在浏览器标签页里打开这个页面,请求就放在链接里;你在那里核对,并用通行密钥签名,页面再通过一个 velawallet:// 链接把签名交回 App。网页版钱包不能用它。
它需要你主动选择。你在创建钱包或登录时把它选为签名方式,从那以后,这个钱包在那台设备上的每一次签名都经过它。它也可以创建钱包的钥匙。在 sign.getvela.app 上,它用的是与各个 App 相同的 getvela.app 通行密钥。
它在签名之前会做什么
- 它自己解码请求。 这个调用做什么、给谁、多少钱,全部从 calldata 里读出来——包括批量交易里嵌套的调用。
- 它只签自己算出来的摘要。 EIP-191、EIP-712、SafeOp 和 SafeMessage 的摘要都在页面里计算,从不采用请求方给的;测试会把 SafeOp 和 SafeMessage 摘要与
vela-core——钱包用的那份代码——对拍,而 App 只接受对它自己算出的那个摘要的签名,其他摘要上的签名一律拒收。 - 它检查这笔交易就是被请求的那一笔。 站点请求的那个调用,必须真的在被签的操作里面,否则页面拒签。
- 它会指出无限授权。 它改不了金额——要么签收到的那些字节,要么什么都不签——所以无限的授权或 permit(2^200 及以上,Permit2 为 2^152 及以上,与 App 同一条线)会以红色显示并注明这个原因,可以原样签署;链上授权的上限,要在请求到达这里之前,在钱包自己的授权界面上选定。对整个 NFT 合集的授权会被拒绝。
- 它拒绝自己担保不了的东西:
eth_sign、它不认识的方法、转给该代币自身合约的代币、它读不懂的操作,以及挑战由请求方提供的登录。 - 它拒绝会交出你账户的请求,规则与 App 相同:账户对自己的所有者、模块、guard 或 fallback 函数发起调用(放在批量交易里也一样);
delegatecall,除非目标是 Safe 的 MultiSend 合约(它把一个操作里的各个调用打包在一起);以及SafeTx签名。它检查的是 App 组装的操作里的每一个调用,而不只是网站请求的那几个。 - 它显示账户的地址,以及在页面上算出的身份图标。 收款方和合约的名字从不取自请求——只有页面自己那份经过审核的表才能给合约命名。账户自己的名字(由 App 发过来,好让你挑对通行密钥)会显示在它的地址旁边。
- 每一次签名都要求用户验证(你的指纹、面容或 PIN)。
它故意没有什么
- 没有任何编辑器。 请求一到就定死了:你要么签,要么不签。费用选择器或者额度编辑器都会重写 calldata,而那正是这个页面要防的病。
- 不联网。 这个页面是一个文件,它的内容安全策略(
default-src 'none')就写在它自己的字节里,所以它取不了任何东西、开不了任何连接,也加载不了图片。唯一离开它的是它的回答——它会跟随请求里的回调链接发出(由 Vela App 发起时是velawallet://)。代币图标用字母画出来。
App 反过来核对什么
App 也不信任这个页面。只有在以下条件全部满足时,它才接受一个签名:被签的挑战就是 App 自己算出的摘要、做过用户验证、钥匙是你钱包的钥匙之一,并且这个 P-256 签名在那把钥匙下验证通过。
每一个已发布的版本,都可以核对
每个版本都从 app-web/trusted-signer/src/ 构建成一个单文件,并且可复现——Bun 和 Node 构建出的字节完全相同——再发布在它自己的地址 sign.getvela.app/b/<sha256>/sign.html,与之前的每一个版本并列。版本列表在 sign.getvela.app/index.json。
cd app-web/trusted-signer
node samples/build-single.mjs --check # rebuilds a version listed in dist/
curl -sL https://sign.getvela.app/b/<sha256>/sign.html | shasum -a 256 桌面版启动时,会取回它将要打开的那个已发布版本,计算哈希,再与自带的版本比对。比对结果只写进日志,不匹配的页面照样会打开。手机端还不做这项检查。
运行你自己的副本
设置里保存着各个 App 打开的那个页面的地址,所以你可以把它指向你自己的部署:任何 HTTPS 地址,或者用于测试的 localhost。用 bun samples/build-single.mjs(或 node)构建,再把 dist/ 复制到你的主机上。
部署在你自己域名上的副本,用的是为那个域名创建的通行密钥签名,而不是 getvela.app 的通行密钥——所以它是创建并使用一个钥匙归属你域名的钱包的方式,而不是替已有的 getvela.app 钱包签名的方式。一个钱包的所有钥匙都属于同一个域名。
代码,以及构建和核对它的脚本,都在 app-web/trusted-signer/ 里。