背背景
metamask 有一个原始的签名方法 eth_sign ,它可以对任意一个字串进行签名,当然也可以对交易 hash 进行签名,这就可能造成诱导签名:
用户在不知情的情况下对一串陌生的 hash 进行了签名,而这串 hash 正是坏人提前组装好的转移(用户)资产的交易的 hash,这就会导致签名账户的资产丢失。
于是, metamask 对使用了 eth_sign 方法的签名界面添加了醒目的红字警告,警示用户这可能是一笔危险的签名。
所以,为了满足真正需要签名的场景,metamask 提供了另一个方法: personal_sign 。这个方法会在待签名的字串 message 前加一个前缀:"\x19Ethereum Signed Message:\n" + len(message)
,这样就杜绝了上文提到的诱导欺骗攻击。
背景
初版的 das-lock 签名是对一个 CKB 交易的 CKB_digest 做 personal_sign 签名。虽然采用了带前缀的 personal_sign 方法,但是 das-lock 在验签时也同样是加了前缀验签的。这实际上是将 personal_sign 重新降级成了 eth_sign ,这又让 CKB 的用户面临着被诱导欺骗的风险。
升级方案
目标
- 将 用户可读的交易内容 或 交易会导致的最终结果 在 metamask 的签名界面上完整展示出来
- 足够的安全
流程
时序图
关键信息流转图
名词说明
名词 | 解释 |
---|---|
CKB_tx_json | 上链的 CKB 交易信息,由后端组装 |
MM_json | 由 CKB_tx_json 精简而来,与 CKB_digest 合体后用于展示在 metamask 的签名页面上 |
CKB_digest | 按照正常的 CKB 签名算法 得到的一个待(CKB)签名的摘要信息 |
MM_hash | 仿照 signTypedData_v4 算法计算出待(metamask)签名的摘要信息 |
MM_sig | 最终由 metamask 签名 MM_hash 后得到的值 |
pub_key | 由 MM_hash 和 MM_sig 通过 secp256k1_ecdsa_recover 还原出公钥 |
lock_args | 由 pub_key 得出 |
步骤说明
步骤 | 校验方 | 校验内容 |
---|---|---|
step0 | type script | 按照 CKB 的签名算法计算出交易的 digest |
step1 | type script | 从 CKB 交易中按照既定的业务规则精简出可供展示的可读的 json |
step2 | type script | 仿照 signTypedData_v4 算法计算出待签名的 hash |
step3 | lock script | 常见的椭圆曲线(NIST) 签名验证 算法 |
其他说明
MM_json
在旧版本 das-lock 中其实就是 "\x19Ethereum Signed Message:\n" + len(message)
,只不过对于任何交易 MM_json
都是这个固定的值,且也不会展示在 metamask 的签名界面上(当然这个内容展示出来也没啥用)。
MM_json
在升级后的 das-lock 中的主要作用就是让用户能清楚地知道自己所做的每一步签名操作到底意味着什么,它的值是随着 CKB_tx_json
的不同而变化的,它会在前端调用 signTypedData_v4 方法时展示在签名界面。
安全性 - 攻防演练
理论上只需要将 MM_sig
上链即可(本来也就是这样的),但是又因为一些其他原因(没有现成的针对 EIP712 计算摘要的 C 库以及其他不趁手的工具),再加上工期和实现成本的考虑,最终决定需要将 MM_sig
和 MM_hash
同时上链。
因为用户私钥由 metamask 管理,前端和后端都不存储任何私钥也不管理任何资产,所以接下来对于安全性的讨论主要集中在合约层面。
防守
按现在的流程,对于合约来讲唯有两个字段是外部输入源: MM_sig
和 MM_hash
。那么合约就需要对这两个字段做有效性和真实性的校验
-
MM_hash
由 type script 校验,由前面的流程图以及时序图里的注解可以知道,type script 本身就会基于当前的 CKB 交易信息重新走一遍前后端曾经走过的逻辑,最后再计算出自己的一个MM_hash'
,最后再和交易里自带的MM_hash
纯字符串比对一下是否相等,不相等则说明MM_hash
有问题。 -
MM_sig
的校验则是一套更通用成熟的椭圆曲线 校验逻辑。跟旧版 das-lock 唯一不同的是,lock script 会直接从 WitnessArgs.lock 里取出经过 type script 校验过的MM_hash
,然后 lock script 用校验后的MM_hash
和传入的MM_sig
一起做签名验证,若验证失败则说明MM_sig
有问题。
攻击
移花接木
试想一下,如果在一个外部恶意的网站中,唤起 metamask 诱导用户签名,展示的签名内容与实际上链的 CKB 交易不同会造成什么后果?
metamask 的签名界面上显示转账 100 CKB,实际上链的交易却转账 200 CKB
这种情况下,metamask 会按照攻击者展示的 MM_json0 来签名,得到 MM_hash0 和 MM_sig0。但这笔交易上链后会在 type script 校验 MM_hash 的过程中失败,因为 type script 会根据当前的 CKB 交易按照既定的 generate 算法生成 MM_hash1,而 MM_hash1 与 MM_hash0 显然是不等的。
接上文,那如果攻击者也按照 generate 算法(合约我们是开源的,任何人都可以拿到 generate 算法)生成一个正确的 MM_hash1,然后将 MM_hash1 和 MM_sig0 上链呢?这种情况虽然会躲过 type script 的校验,但是会在 lock script 的校验那里失败,因为 MM_hash1 和 MM_sig0 根本就不是带有羁绊的明文和密文。
接上文,那如果攻击者将 MM_hash1 + MM_hash0 和 MM_sig0 同时上链试图分别通过两个脚本的校验的话。这种情况下只要 type script 和 lock script 采用了相同的 extract 逻辑,那么这种交易也同样无法同时通过两个 script 的验证。
交易重放
由于我们需要 MM_json 是可读且有大小限制的,所以就不可能完全 copy 真实的 CKB 交易结构,这就注定了 generate 算法是不可逆的,不可逆意味着必定有信息的丢失。那么攻击者就可能会利用这个信息差来复制之前用户旧的合法的交易来尝试重放攻击。
攻击者先在链上找到一笔用户正常的 CKB 交易,然后利用 generate 算法的信息丢失的特点精心构造一笔假的 CKB 交易,使得这两笔交易 generate 出来的 MM_hash 是一致的,然后直接 copy 正常交易里的 MM_hash 和 MM_sig 填入伪造的交易里,最后直接进行一个上链操作……
好了,这就是为什么时序图里有 instert(MM_json_A, digest) 这一步操作的原因了。CKB_digest 就是为了弥补 generate 算法的信息丢失特点而存在的,CKB_digest 是 CKB 交易的摘要信息,哪怕 CKB 交易里只改变一个字母,CKB_digest 都会变得面目全非。
有了 CKB_digest 既能让 generate 算法的设计者放心大胆地去满足需求,同时也不会让系统的安全性打折扣。
欺骗诱导
试想下面的场景:
用户在某个 ETH 的菠菜网站上玩猜大小游戏,网站弹出提示说:为了保证游戏的公平公正,需要你对平台生成的随机数做一个签名,然后再将签名后的结果作为下把游戏的随机的种子。
随机种子由平台和用户共同随机生成,嗯嗯~这似乎合情合理(其实攻击者还能设计出比这更绝妙更合理的诱导用户签名的场景出来)。让人看起来用户在玩 ETH 上的菠菜游戏,其实隔壁 CKB 链上的资产都已经被掏空了。甚至攻击者都不需要立即转移资产,先等个七天后再将交易上链,用户甚至连案发时间和案发地点都说不清,更别说破案了,怎么死的都不知道。
对于旧版的 personal_sign 签名方案,用户即便有安全意识也没有渠道去校验这笔交易,因为展示在 metamask 界面上的是一串不友好的 hash。
而现在用了升级版的 das-lock 后,会将详细交易信息(MM_json)展示在签名界面上,而且除此之外我们还会对 das-lock 支持的场景再给一个对于小白用户更加可读的(以自然语言组织的)语义化后的文本信息,这也会展示在 metamask 的签名界面上(字段名为 DAS_MESSAGE)。
升级节奏
新版 das-lock 上线后,新注册的 das 账户自动就会享受到更安全的 EIP712 的算法,旧版的账户,只要做一次下面的任意一次操作便可以升级为新版的 das-lock:
- 转移 manager (只升级 manager)
- 转移 owner
之后会考虑逐渐将旧的 das-lock 无感知地升级成新版 das-lock。
以转账交易和编辑解析记录交易为例,升级后的签名界面如图: