作者:Stepan Snigirev
本文为 Stepan Snigirev 在 TABConf 2022 上的演讲,由 Michael Folkson 转写为文字稿。
开场(Jeff Gallas)
我非常高兴地宣布我们今天的第一位演讲者,来自 Specter 的 Stepan Snigirev,他是 Specter Solutions 的 CTO,有 3 年的开发比特币软件钱包(soft wallets)和硬件签名器(hard wallets)的经历。欢迎 Stepan。
概述(Stepan Snigirev)
我今天的演讲主题是 “在硬件签名器上支持 Taproot”。我们刚刚激活了 Taproot,非常棒,是在去年 11 月激活的。一些软件钱包已经开始集成了,而且甚至一些硬件签名器也开始了集成了。现在大部分人用的都是单调的 “单密钥、单签名” 方案。我想讲讲我们可以用 Taproot 做什么。我觉得应该大家都知道了,所以我会讲快一点,然后我会讨论为什么在硬件签名器中集成 Taproot 是非常困难的、难点在哪里。如果我们无法在硬件钱包中集成,我们还有什么办法?
通过隐藏实现隐私
Taproot 非常出色。首先是它给了用户隐私性。在你观察区块链的时候,如果你看到一个单签名和但公钥的 taproot 地址,它里面可能实际上是一个公钥和一个脚本树。然后,这个公钥自身也可能凝结了一组公钥,而这棵脚本树可能非常高,是许许多多脚本的复杂集合。在里面你可以放置任意类型的时间锁,然后备份平时不会用到的私钥、仅在紧急情况下才启用它们。这意味着,所有复杂的花费条件,在链上看起来都是一样的。这是非常棒的事。甚至放在脚本中的公钥也可以代表着一组公钥,这就像是无限阶的密钥聚合。非常酷。
Miniscript(更安全的明文备份)
我个人会使用它的第一个理由是,它支持更好的明文备份。为什么现在没有人使用 Miniscript 或者复杂的比特币脚本?首先是因为比特币脚本复杂而不容易编写(在 Miniscript 出现之前)。其次是所有人都不使用它。这是一个鸡生蛋还是蛋生鸡的问题:每个人(90%)都使用单签名脚本,10% 的人使用多签名脚本,只有 0.3% 的人使用定制化的脚本。如果你使用一些定制化的脚本,你就暴露在了这 0.3% 里面。所有的连分析公司都知道,要是使用这样的脚本,那很有可能是同一个人。这样的隐私性非常糟,这就是障碍之一。
花费条件:or(HW, and(backup, timelock))
描述符:tr(HW, {and_v(v:pk(backup), older(timelock))})
Tapscript:<backup> OP_CHECKSIGVERIFY <timelock> OP_CHECKSEQUENCEVERIFY
(译者注:这段花费条件的意思是:一个硬件签名器随时可以花费这笔钱;同时,时间锁过期后,后备私钥也可以花费这笔钱。)
我个人会使用,我非常害怕把明文的钱包复原词(recovery phrases,应指种子词)放在我家里。如果有人得到了它,那我的钱就全部丢了。我个人的做法是使用一个不备份的硬件钱包,然后设置一个备用脚本,这个备用脚本带有时间锁,加上复原词就可以花费我的钱。然后,如果我遇到了什么意外,或者我的硬件钱包坏了,那么等待一段时间(也许半年)我就能拿回我的钱。但是,如果我的复原词被盗了,他们是没法立即偷走我的钱的(如果硬件钱包还在我手上的话)。我有足够多的时间,讲资金迁移到一个新的装置上。但是,想想硬件签名器和 Miniscript 实现,现在还没有一个东西真正支持这个功能。太糟糕了。但实际上这并不是很难。在我给我们的硬件签名器集成 Miniscript 的时候,基本上我只花了一周的时间。我只要坐下来就可以开始开发了,因为它的说明真的写得非常好。Miniscript 有两个元素,其中一个你可以忽略,另一个是,如果你有一段可读的 policy 表达式,你就可以把它转成钱包的描述符。这有点复杂,但你不需要在硬件签名器里完成操作。第二部分是将钱包的描述符编译成实际的比特币脚本。这基本上只是把这些记号替换成比特币脚本的操作码,然后把派生出来的密钥放在正确的位置上。非常简单。然后硬件签名器就可以确定哪个输出是找零,并验证找零输出是从相同的描述符中派生出来的。这就行了。我想提一句,Ledger 团体最近做了很多工作来升级他们的比特币应用。他们在设计的时候就采用了 Miniscript 方法。虽然现在仅支持多签名功能,但很容易就能升级到支持定制化的 Miniscript,所以我很期待。至于硬件签名器,我不知道他们的计划。但至少会有两种硬件签名器将支持 Miniscript。
交互式多签名的 xpub
xpub = {c, P}{c, P} = {h1(c, P, i) , P + h2(c, P, i)}
另一个应用场景是,假定你开了一家合伙的托管公司。你的客户可以在自己的多签名装置中使用你的公钥。举个例子,他们可以制作 2-of-3 的多签名装置,其中 2 个密钥由自己控制,但还有 1 个密钥是你们公司的。你不想让这个密钥出现单点故障,所以你虽然给出了一个公钥,但你不希望它只是一个公钥。你有一个办法:交互式的多签名。也就是将多个公钥聚合成一个公钥。遵循了特定的签名流程,你就只需向用户给出一个 xpub(公钥)。这是兼容 BIP32 以及 xpub 的派生方法的。它只是用一个基于哈希值的标量来调整你的公钥而已。你只需要提前把多个公钥组合起来就好。在你构造这个 xpub 时,你要取得 “链码(chaincode,用在 BIP32 密钥派生中的数据)”,然后运行 XOR 运算,将这个公钥与一个常规的 MuSig 或你用的其它协议的公钥结合起来;在你需要派生一个新的子私钥时,只需这个聚合链码以及公钥,就可以派生出下一个公钥。你能够使用多个设备执行交互式签名,来生成所需的正确签名。这是非常棒的应用。
Taproot 的难点:交互式多签名领域
但是,就像我说的,你需要以来这种交互式的多签名流程。如果你读过论文,你可以数一数,建立交互式的多签名有多少种方法。就我所知,至少 5 种:MuSig、MuSig2、FROST、MuSig-DN、GKMN21 。这意味着,每一种方法都有自己的取舍。每一种都有自己的安全边界。我想在整体上介绍一下它们,并讲讲实现它们的难处在哪里。我应该先帮大家回顾一下 Schnorr 签名。
Schnorr 签名:
选取 nonce
r
,R = rG
sig(签名) = {R, d.hash(P, R, m) + r} x G = P. hash(P, R, m) + R
聚合签名:
P = sum of a_k P_k
R = sum of R_k = sum of r_k.G
sig_k = {R, a_k.d_k . hash(P, R, m) + r_k}sig = {R, d.hash(P, R, m) + r} xG P.hash(P, R, m) + R
(译者注:此处的记号有一些混乱。但下文的介绍是清楚的。)
如果你要签名,那么你先要选出一个随机的 nonce;你将这个 nonce 的公钥点跟签名公钥和待签名消息一起哈希,然后与你的签名私钥相乘,最后加上这个随机 nonce 值,以在签名中隐藏你的私钥,这样就没有人能计算出你的私钥。然后,验证者只需要得到你的签名,将它与生成器点相乘,就可以验证等式是否成立。如果你要在此基础上建立多签名,流程也是一样的,但会遇到一些问题。
第一个问题是放在哈希函数中的 nonce 值,每个联合签名人都要生成自己的 nonce 值。第一个要求是,他们要相互通信,告知他人自己要使用什么 nonce 值。只有每个人都得到聚合的 nonce 值之后,才能各自生成碎片签名。然后,举个例子,软件钱包可以将它们(所有碎片签名)加在一起,就得到了最终的签名。这个方案还不算是非常复杂,你只需要额外的一轮通信,来沟通 nonce。
那为什么会有这么多论文呢?先说 MuSig,它可能是人们提出的第一篇论文。它实现了 n-of-n 条件下的密钥聚合。要么是 2-of-2,要么是 3-of-3、5-of-5,你不能做 2-of-3 或者 3-of-5。它需要 3 轮通信。第一轮是选择 nonce 值(R_i = r_i.G
),你哈希它(hash(R_i)
)并把它发送给其他人。这是在承诺 nonce 值,就像在说:“这是我所用的 nonce 值的哈希值,我先放出来,我待会就会使用它所对应的 nonce 值,我不能再换用别的 nonce 值。”然后,人们发送 nonce 值本身(R_i
),从而得到所有联合签名人的 nonce 值,并生成聚合 nonce 值(R = sum of R_i
)。三轮通信很可怕,假设你有三个分散再不同地方的硬件签名器,那么,你需要跑 3 躺。而且,在此期间,每一个签名器都要保存状态,需要记得所有的承诺,等等。但当前的硬件签名器并不是这样工作的。这些签名器的设计目标是成为无状态的东西,不希望有交互。
R’_i = r’_i x G, R’’_i = r’’_i x GR’_i, R’’_iR = sum of (R’_i + b_i.R’’_i)sig_i
然后是 MuSig2,是 MuSig2 的升级版,移除了第一轮承诺通信,但是,你就不是只生成一个 nonce 值了,你生成了两个。然后,你将这个 nonce 值跟其他人分享,并将它与某个基于哈希值的系数相乘。我不想讲太多细节,但这样一来,它的安全假设就多加了一条。如果你要让它变得更安全,你要生成 4 个 nonce 值。这跟一些麻烦的密码学有关,比如分叉引理(Foring Lemma)、时间机器什么的。如果你想搞清楚细节,我建议你读读论文。基本上,通过生成一个额外的 nonce 值来制作安全性,你就可以减少一轮通信。两轮已经好很多了。你可以提前生成 nonce 值。你可以从每一个硬件签名器中生成 100 个 nonce 值,然后在你需要的时候使用。然后你只需要为每个签名器跑一趟就行了。再说一遍,它也需要签名器保存状态。硬件签名器需要知道自己所生成的 nonce 值,并验证没有重复使用相同的 nonce 。如果你用了相同的 nonce ,那你的私钥会被人计算出来。
最后是 FROST。FROST 很有趣,因为它并不集中在 nonce 生成上,它瞄准的是密钥的聚合。你可以实现 2-of-3、3-of-5 的聚合密钥。这里的想法是使用 Shamir 的可验证私钥分割方案(verifiable secret sharing scheme)。如果你想实现 2-of-3,假定你有 3 个私钥,并且这 3 个私钥在同一条线(on the same line),那你只需要其中 2 个,就可以派生出这个 结合私钥/公钥,以及签名。这三个点中的任意两个,都可以帮助你重新构造最终的签名。这意味着,唯一的问题在于,如何确保我们随机生成的私钥最终会在同一行。而 FROST 论文的主要想法就是如何在多个签名者之间沟通,使得他们最终会在同一条线上。这是一个交互式的方案,但非常好,因为在第一次交互式启动之后,你就可以将它抛在脑后,只需要 2 轮通信就可以了。但是,再说一遍,这 3 种方案都要求 nonce 值不能复用。这意味着,为了生成新的 nonce 值,要么你需要一个计数器,要么需要一个随机数生成器(RNG)。这两种东西在硬件签名器种都是问题,因为我可以黑了计数器,也可以劫持随机数生成器。我给你们看看怎么做到。
优点:非常简单就能实现,2 轮通信,接近于不需要交互。
缺点:重度依赖于 RNG 或者计数器,需要保存状态。
Taproot 的难点:为多签名派生 nonce
第一种想法是使用计数器。也就是一个只增不减、永不重置的数字;永远不会使用相同的数值,你将这个数值与你的私钥一起运行哈希函数,就可以得到 nonce 值。
递增型计数器:counter ++ ; r = hash(d, counter)
每次你使用的时候,就增加一下计数器的值。理论上它是很好用的。但在现实中有许多攻击,比如 Fraunhofer Institute 解锁了一个微控制器,然后发动了攻击。他们将一束激光照射进控制器,要么计数器会归零,要么激光照到正确的位置,它会减一次,从而导致相同的 nonce 值被再一次使用。这里的问题是,签名中的 nonce 值会进入区块链,而且你的联合签名人也会知道,软件钱包也会知道。这意味着,要是 nonce 值没有足够的熵,或者被重复使用,他们就可以从签名中计算出你的私钥。至于随机数生成器,DEF Con 熵有一个非常棒的演讲,用 45 分钟讲了随机数生成器的问题。这意味着,即使你使用了一个有证书(闭源的)随机数生成器,它一般来说也是不够用的。这个演讲大部分的内容都关于作为一个开发者,你可能会怎样搞砸随机数生成器。甚至还没讲到随机数生成器被黑的问题。
Taproot 的难点:打破随机数生成器(等待坏的 RNG 输出)
所以,你能怎么搞砸随机数生成器呢?首先,你无法控制随机数生成器会面临的环境。索尼的 Playstation 2 就被黑了,因为索尼重复使用了相同的 nonce,导致私钥被泄露,然后你就可以拿这个索尼私钥在家里自制 Playstation 2 了。Yubikey,当他们想要通过认证流程时,他们确实通过了,但搞砸了随机数生成器的初始化。只需要 3 个签名,就可以计算出他们的私钥。就像我在这个演讲中说的,有人分析了来自两家微控制器的随机数生成器的输出。他们说,有时候,你会意外得到一堆全是 0 的 nonce,这就完蛋了。有时候,它会多次给你相同的数值,如果你请求随机数过于频繁的话。它就是不能生成一个新的随机数。还有一些时候,RNG 会出错,因为电压故障或者一些神秘的原因(比如太阳烤热了微控制器)。另外说语句,这样的事情在计数器上也可能发生。要是这发生在你的使用了计数器的硬件签名器上,你就真的遇到很大一件事了。这就是搬起石头砸自己的脚。
Taproot 的难点:打破随机数生成器(影响 RNG 操作)
我们再假设有人就想砸你的脚。那你会如何中招呢?这是最常见的随机数生成器架构,一个环形震荡器(ring oscillator)。基本上,它使用了标准的 “非” 门,这是很容易用半导体实现的。非门的作用是将 0 变成 1,将 1 变成 0 。你将 3 个非门连接在一起,这样你会得到一些时延。然后,你又将第三个非门的输出当成第一个非门的输入。这就成了一个荒谬的逻辑电路,它会不断在 0 和 1 之间跳来跳去,而切换的时机将高度依赖于环境、制造缺陷、半导体的杂志,等等。所以它基本上会给你一个完全无法预测的输出。为了得到更加随机的输出,你可以把一堆这样的振荡器放在一起,运行 XOR 操作。现在,你回忆一下你在高中或者初中上过的物理课,如果你把多个钟摆放在一根杆子上,会怎么样?不会怎么样,因为它是一根杆子,这些钟摆都会以自己的频率摆动,但是如果把它们放在一根绳子上呢?绳子会让能量在这些钟摆间传递。所以一段时间之后,这些钟摆会同步 —— 摆动的频率变得相同。你在 YouTube 上搜索一下视频吧,看看是怎么回事。问题来了,如果你将多个振荡器放在一起,那么最终它们会同步,然后你的随机输出就将不再随机。在得到了认证的、设计良好的 RNG 中,会有一些应对措施来检查输出是否良好。但是,如果你只是把自己的微控制器放在 PCB 板上,而且设计很差,有一个路径可以通过所有这些控制器,那会怎么样?它会把这种耦合重新带回来。有一种攻击是盗取你的设备、拆解它、在里面放一些电线,也是一样的道理 —— 引入耦合。然后你就完蛋了。
另一种随机数生成也不是完美的。如果你使用的是一个依赖于温度的东西,那我们可以把它冻起来。你还可以调低随机数生成器的电压,然后它就会输出更多的 0,输出更少的 1,或者产生一些奇怪的东西。这些奇怪的东西也是低熵的。没有足够的熵,你的 nonce 就会被暴力破解,然后你就完了。还有各种各样的错误注入,我可以拿一个电磁表,迫使这些振荡器或者说随机数生成器都失灵。所以随机数生成器是糟糕的,至少不完美。
Taproot 的难点:交互式多签名领域总结
有没有一种解决方案呢?可以看到有 5 篇论文。在第二列中,有两篇论文不需要随机数生成器。他们使用确定性的 nonce,而且不止是确定性的,还是可验证的确定性 nonce。这意味着,你的硬件签名器可以生成 nonce 值并向他人证明它是使用某一种算法确定性地生成出来的。MuSig-DN 是使用了确定性 nonce 的 MuSig。GKMN21(Garillot、Kondi、Mohassel、Nikolaenko)来自 Facebook,他们确实出版了这篇论文,而且写得非常好。看起来有一个非常棒得解决方案。唯一得问题是,生成这些证据(证明你得 nonce 是确定性得)是非常复杂得。比如在 MuSig-DN 的基准测试中,如果你在一个英特尔的 i7 、频率为 3Ghz 的核心计算器上运行(这是家用计算机的配置),它需要 1 秒来生成证据。考虑到硬件签名器一般只有 100 MHz,而且是 32 位的,不是 64 位的,你可能要乘以 100。
交互式多签名方案比较
这里作一个比较。前面三种方案(MuSig、MuSgi2 和 FROST)依赖于 RNG,所以我们先不管基准测试,它肯定更快。但后面的两种(MuSig-DN,GKMN21),MuSig-DN 在微控制器上可能需要 100 秒。如果你有一笔 5 个输入的交易,你可能需要等待 10 分钟,这就不舒服了。而且内存要求也很高。我认为可以优化,但依然需要微控制器有 10 MB 的内存。现在的签名器一般来说是 100 KB,左右吧。也许在高端设备上可以有 MB 级别的内存。比如 Keystone 是基于 Android 系统的,它有大量的内存,但他们在安全芯片(security element)上运行的安全代码也不是非常高效。证据的大小还行,1KB 。我喜欢 QR 码,所以我不喜欢通过 QR 码来传输 1KB,那会很复杂,但还好。最后,第二篇论文(GKMN21)试用乐坤垂死零知识证明,所以它会快很多,而且在微控制器上也可以运行。内存要求我不确定,但我认为也要用到几 MB。证据的体积是 1MB。相当于一整个比特币区块的大小。但你不需要广播它,只需要在签名人之间传播。
这就是我对所有的多签名方案的总结。每一种都有特定的取舍。我会说,如果你使用了多个硬件签名器,而且不想让它们在同一时间连接同一台电脑,比如它们是分开保管的,那么别用交互式多签名方案。使用常规的多签名就好,知道我们得到更合理的 MuSig 实现。但是,确实有一些用途,是非常有用的。
Taproot:现在可以用在哪里?
比如说闪电网络,它有完全不同的安全模型,你的私钥一直是触网的。无论如何你都需要保存状态。所以使用 MuSig 并不会增加你的攻击界面。所以用在闪电通道中就很好。原子化互换,可以在热钱包中完成,所以情况也非常类似。也没问题。然后是服务端是签名者之一的情况。比如 Blockstream Green 钱包,使用 2-of-2 或者 1-of-2 加上时间锁的装置。使用 Taproot 也完全是一种优化。Muun 钱包使用 2-of-2 多签名,Square 也在使用服务端做一件跟移动钱包和安全私钥管理相关的有趣的事。都是非常好的应用场景。最后是我最喜欢的东西,我的梦想、我的激情、我的宝贝。我梦想了 3 年了,一直没有时间去实现。一个 paranoid HSM(硬件安全模块),将多个芯片组合在一个设备上,使用完全开源的 RISC-V PGA 板,使用基于 NDA 的安全芯片(比如英飞凌的),还有一些其它的基于 RAM 的微控制器。每一个芯片都有一个私钥,每一个都必须签名才能得到一个完整的签名。然后,如果攻击者希望黑掉这个东西,他们需要黑掉 3 种不同的微控制器。这非常非常棒,尤其是对 HSM 企业用户来说。企业也可以将 HSM 放在一个法拉第笼(Faraday cage)中,并使用一根导线,来检测所有篡改的尝试,等等。这非常酷。我觉得 Taproot 真是太令人惊讶了。我们可以翘首等待它的演化,太棒了。
问答
问:闪电钱包现在就可以用到 Taproot 多签名了吗?
答:现在应该连规范都还没有,所以应该也还没有软件实现。c-lightning 应该在往这个方向开发,但最好问问 Christian Decker,他也在这里。应该还在编写规范的阶段。但总的来说,它意味着,你的开启通道和共同关闭通道的操作,在链上都只会表现位一个单签名以及单公钥。仅在单方面关闭通道的时候,才会暴露你的 Taptree 上有一个时间锁。
问:你认为上面的合作式多签名方案中,哪一种能得到主流采用?我们现在的情况很尴尬,已经有 14 种标准了,我们需要制定一个最终的标准,结果是出现了 15 种标准。
答:我不知道哪一种是每个人都会用的。我个人喜欢 MuSig,可能还有它跟 FROST 的某种结合,因为它实现了门限签名。从我的理解来说,FROST 会有交叉输入集成(cross input integration)的问题,Sanket 已经提到了。坦白说我也不知道。我认为从 MuSig 开始理解这个领域是对的,因为它可能也是第一种被采用的。最好问问开发 libsecp256k1 的人。我没有一个确定的答案。
问:哪一种硬件签名器允许你自己为 nonce 提供熵吗?如果没有,为什么没有呢?
答:在 Taproot 以前,比特币社区讨论硬件签名器时,讨论的是选择 nonce 攻击(chosen nonce attacks)、侧信道攻击(side channel)的验证和缓解,这些会在硬件签名器被劫持时导致你的私钥泄露。我知道两种硬件签名器在签名种实现了额外熵的混合,是 Jade 和 Bitbox 。就我所知,迄今为止没有别人做了这个。但这对 MuSig 的第一步来说非常重要。我认为,我们可以为 MuSig 复用相同的规范和协议。只要其它硬件钱包开始了,当他们开始实现 MuSig 的时候,他们就需要这个 API 来混合额外的熵。
问:你提到了 RISC-V。你认为是否有某种定制化指令,可以让这些变得非常快吗?基本上你必须在软件实现种计算所有东西。
答:在 FPGA 上的 RISC-V 吗?我推进你看看来自 Bunnie 的 Precursor 项目,供记者使用的安全通讯设备以及别的东西正在众筹。现在他们正在制造。他们非常了解硬件,他们使用 FPGA,并在 FPGA 上使用 RISC-V 核心,所以你可以看看。他们也有一套操作系统,以及一些安全操作。理论上来说他们也是完全开源的。我们可以做的就是把他们做的一切都拿过来然后为我们所用。我认为开源的 RISC-V 核心发展得非常快,而且更接近量产状态了。在这个设置中,比如说,你并不需要真正到达可量产状态,因为你的安全性还依赖于其它芯片。有一些 FPGA 制造商位于中国,我不知道你信不信得过。它们非常便宜,而且可以在最小实现上运行 RISC-V。总的来说,有了 FPGA,你想要运行 RISC-V 并获得良好的速度,你需要支付一些溢价,因为 FPGA 更贵。可能几百美元吧。
(完)