[TOC]
前言
原文地址:https://www.jianshu.com/p/941de6013a64
作者:梦幻艾斯
备注:欢迎转载,请保留原文地址。
在学习BIP32协议的时候遇到了不少疑惑,为了以后自己回顾方便。现在把遇到的问题和自己的见解写下来。全文先将BIP32协议进行翻译然后进行解读。解读之前先简单介绍下这个协议。
简介
BIP32协议是一个密钥生成协议。用户可以通过一个随机种子生成一个扩展私钥,然后通过这个扩展私钥生成N多私钥、公钥对和N多子扩展私钥。这就解决了钱包存储不方便的问题。具体看协议中的动机篇
协议解读
以下内容会对协议的重点内容进行翻译并解读,原文地址
https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki
====================== 翻译并解读 ================
摘要(Abstract)
该文件描述了分层确定性钱包(或“HD钱包”):可以部分或全部用不同系统共享的钱包,每个钱币具有或不具有花费硬币的能力。
该规范旨在为可在不同客户端之间互换的确定性钱包设定标准。虽然这里描述的钱包有很多特点,但并不是所有的支持客户都需要。
该规范由两部分组成。在第一部分中,介绍了用于从单个种子推导密钥对的树的系统。第二部分演示了如何在这样的树之上构建钱包结构。
注:这里写下对非确定性钱包、确定性钱包、分层确定性钱包的理解
- 非确定性钱包--一堆随机生成的密钥对的集合
- 确定性钱包--由一个种子生成的密钥对的集合
- 分层确定性钱包--由一个种子生成扩展密钥(私钥,链码),扩展密钥可以生成多个密钥对还可以生成多个字扩展密钥
协议要解决的问题(Motivation)
比特币标准客户端使用随机生成的密钥。为了避免在每次交易之后对钱包进行备份,(默认情况下)客户端将生成100个密钥缓存在备用密钥池中。这种钱包并不打算分享私钥和在不同的系统使用。(注:这句翻译的有点虚,请看原文)。它们支出使用钱包加密功能去隐藏它们的私钥而不是共享密码。(解读:这种钱包备份的时候都是备份一个经过加密的文件,恢复的时候从加密文件中恢复),但这些“中性”钱包也失去了生成公钥的权力。(注:这条解决了非确定性钱包的问题)
确定性钱包不需要如此频繁的备份,而椭圆曲线数学允许在不泄露私钥的情况下计算公钥的方案。这允许例如网上商店业务让其网络服务器为每个订单或每个客户生成新的地址(公共密钥散列),而不会让网络服务器访问相应的私人密钥(这是花费收到的资金所需的)。
然而,确定性钱包通常由一对钥匙对组成。只有一个链条意味着分享钱包的时候被分享者可以获得你钱包的所有控制权。但是有些时候我们只需要分享部分共钥。在网上商店的例子中,网络服务器不需要访问商人钱包的所有公共密钥; 仅限于那些用于接收客户付款的地址,而不是例如当商家花钱时产生的更改地址。分层确定性钱包允许通过支持从单根生成的多个密钥对链来实现此类选择性共享。(注:这条解决了确定性钱包的问题)
规范:重点公式推导
约定
在本文的其余部分中,我们将假设比特币中使用的公钥密码术,即使用由secp256k1(http://www.secg.org/sec2-v2.pdf)
定义的字段和曲线参数的椭圆曲线密码术。以下变量是:
- 整数取模曲线的阶(称为n)。
- 曲线上点的坐标。
- 字节序列。
(注:描述一条Fp上的椭圆曲线,常用到六个参量: T=(p,a,b,G,n,h)。详情请看比特币学习1--椭圆曲线算法)
两个坐标对的加法(+)定义为EC组操作的应用。连接(||)是将一个字节序列附加到另一个字节序列的操作。
作为标准转换函数,我们假设:
point(p):返回由整数p表示的secp256k1基点的EC点乘法(EC组操作的重复应用)产生的坐标对。
ser32(i):将32位无符号整数i序列化为4字节序列,大端存储(计算机术语)。
ser256(p):将整数p序列化为32字节序列,大端存储(计算机术语)。
serP(P):使用SEC1的压缩格式将坐标对P=(x,y)串行化为字节序列:(0x02或0x03)|| ser256(x),其中头字节取决于省略的y坐标的奇偶校验。偶数为0x02,奇数为0x03
parse256(p):将32字节序列转换为256位数,大端存储(计算机术语)。
扩展密钥(Extended keys)
在接下来的内容中,我们将定义一个函数,该函数从父key中派生出一些子key。为了防止这些仅依赖于父key本身,我们首先扩展私钥和公钥以及额外的256位熵。这种扩展称为链码。对于相应的私钥和公钥链码是相同的,并且由32个字节组成。
我们将扩展的私钥表示为(k,c),其中k是普通私钥,c是链码。一个扩展的公钥被表示为(K,c),其中K =point(k),c是链码
每个扩展密钥有231个普通子密钥和2 31个强化子密钥。每个这些子key都有一个索引。正常的子key使用索引0到231 -1。硬化的子key使用索引231至232 -1。为了简化对硬化的密钥索引的记号,数字iH表示i + 231。
子key推导方法
给定父扩展密钥和索引i,可以计算相应的子扩展密钥。这个算法依赖于父扩展密钥是私钥扩展还是公钥扩展,i是普通索引还是硬化索引(i >= 231)
父私钥->子私钥
函数CKDpriv((kpar,cpar),i)→(ki,ci)从父扩展私钥计算子扩展私钥:
- 检查 i ≥ 231 (判断这个子key是不是硬化子key).
- 如果是 (硬化子key): 推导公式为 I = HMAC-SHA512(Key = cpar, Data = 0x00 || ser256(kpar) || ser32(i)). (注意: 使用 0x00 填充私钥使他的长度达到 33 bytes.)
- 如果不是 (普通子key): 推导公式为 I = HMAC-SHA512(Key = cpar, Data = serP(point(kpar)) || ser32(i)).
- 将I分割成两个 32-byte 序列, IL and IR.
- 得到的子私钥 ki = parse256(IL) + kpar (mod n).
- 得到的链码 ci = IR.
- 当 parse256(IL) ≥ n or ki = 0, 推导出来的子 key 是无效的, 并且我们应该把索引i设成下一个值. (注意: 在2127中发生这种情况的概率小于1 .)
HMAC-SHA512 函数定义在 [http://tools.ietf.org/html/rfc4231 RFC 4231].
父公钥->子公钥
函数 CKDpub((Kpar, cpar), i) → (Ki, ci) 从一个父扩展公钥计算出一个子扩展公钥. 仅适用于普通子key(索引i满足0<= i <231)
- 检查 i ≥ 231 (这个子key是否是硬化子key).
- 如果是 (硬化子key): 返回失败
- 如果不是(普通子key): 推导公式为 I = HMAC-SHA512(Key = cpar, Data = serP(Kpar) || ser32(i)).
- 将I分割成两个 32-byte 序列, IL and IR.
- 得到的子公钥 Ki = point(parse256(IL)) + Kpar.
- 得到的链码 ci = IR.
- 当 parse256(IL) ≥ n or Ki 是无穷远点时, 推导出来的子key是无效的, 并且我们应该把索引i设成下一个值.
父私钥->子公钥
定义:函数 N((k, c)) → (K, c) 计算出一个扩展私钥对应的扩展公钥 (作为中间版本,移除了签名交易的功能).
- 返回的 K = point(k).
- 链码 c 不变.
从父私钥计算子公钥的方法:
- N(CKDpriv((kpar, cpar), i)) (总是有效的).
- CKDpub(N(kpar, cpar), i) (仅对普通子key有效).
The fact that they are equivalent is what makes non-hardened keys useful (one can derive child public keys of a given parent key without knowing any private key), and also what distinguishes them from hardened keys. The reason for not always using non-hardened keys (which are more useful) is security; see further for more information.
在不知道父私钥的情况下能从父公钥推导出子公钥,这就是非硬化keys有用之处,也是和硬化keys区别的地方。那为什么不总是用非硬化keys呢?这涉及到安全问题,详情请看后面的内容。(注:这段翻译有点虚,附上原文)
父公钥->子私钥
不可能实现
============ 子key推导方法疑惑解读 开始 ============
-
父公钥->子公钥推导过程中从父公钥推导出子公钥。我们知道每一个公钥唯一对应一个私钥,那么子公钥对应的私钥到底是什么呢?(我刚看这个协议的时候这个问题困惑我很久)
先给出答案:父公钥->子公钥推导出的公钥对应的私钥就是父私钥->子私钥中推导出来的私钥。下面我们进行公式证明。
-
父私钥->子私钥推导公式为 Ik = HMAC-SHA512(Key = cpar, Data = serP(point(kpar)) || ser32(i))
子私钥 ki = parse256(IkL) + kpar (mod n).
-
父公钥->子公钥推导公式为 IK = HMAC-SHA512(Key = cpar, Data = serP(Kpar) || ser32(i)).
子公钥Ki = point(parse256(IKL)) + Kpar.
因为Kpar = point(kpar),所以得出结论Ik等于IK
由公钥推导公式K=kG=point(k),子私钥乘以G得
kiG = parse256(IkL)G + kpar (mod n)*G
K=point(parse256(IkL)) + Kpar =point(parse256(IKL)) + Kpar
证明完毕
-
非硬化key父公钥推导子公钥的安全问题:
访问扩展公共钥匙并不能得到访问子私人密钥的途径。但是,因为扩展公共钥匙包含有链码,如果子私钥被知道或者被泄漏的话,链码就可以被用来衍生所有的其他子私钥。一个简单地泄露的私钥以及一个母链码,可以暴露所有的子密钥。更糟糕的是,子私钥与母链码可以用来推断母私钥。
解决办法:为了避免了推到出主钥匙,主钥匙所衍生的第一层级的子钥匙最好使用强化衍生。
具体参考:《精通比特币-第四章》
============ 子key推导方法疑惑解读 结束 ============
key树
下一步是级联几个CKD结构来构建一棵树。我们从一个根开始,即主扩展密钥m。通过评估CKDpriv(m,i)对于i的多个值,我们得到许多1级派生节点。由于这些节点都是扩展密钥,CKDpriv也可以应用于这些。
为了缩短符号,我们将写成CKDpriv(CKDpriv(CKDpriv(m,3 H),2),5)为m / 3 H / 2/5 。等同于公钥,我们将CKDpub(CKDpub(CKDpub(M,3),2),5)写为M / 3/2/5。这导致以下身份:
- N(m/a/b/c) = N(m/a/b)/c = N(m/a)/b/c = N(m)/a/b/c = M/a/b/c.
- N(m/aH/b/c) = N(m/aH/b)/c = N(m/aH)/b/c.
但是 N(m/aH) 不能写成 N(m)/aH, 因为后者是不可能实现的 .从父公钥->子公钥算法中我们知道硬化子建是不能用公钥推导子公钥的
树中的每个叶节点对应一个实际的键,而内部节点对应于从它们下降的键的集合。叶节点的链代码被忽略,只有它们的嵌入私钥或公钥是相关的。由于这种结构,知道扩展私钥允许重建所有后裔私钥和公钥,并且知道扩展公钥允许重建所有后裔非加密公钥。
key标识符
扩展密钥可以通过序列化ECDSA公钥K的Hash160(SHA256之后的RIPEMD160)来识别,忽略链码。这与传统比特币地址中使用的数据完全一致。不建议以base58格式表示这些数据,因为它可能被解释为一种地址(并且钱包软件不需要接受对链式密钥本身的付款)。
标识符的前32位称为密钥指纹。
序列化格式
扩展公钥、私钥序列化格式如下
4字节:版本字节(mainnet:0x0488B21E public,0x0488ADE4 private; testnet:0x043587CF public,0x04358394 private)
1字节:深度:主节点为0x00,级别1派生密钥为0x01。
4字节:父密钥的指纹(如果主密钥为0x00000000)
4字节:子数字。这是对于i在xi = xpar / i中的ser32(i),其中xi是键序列化。 (如果主密钥为0x00000000)
32字节:链码
33字节:公钥或私钥数据(公钥的serP(K),私钥的0x00 || ser256(k))
这78字节结构可以像其他比特币数据一样使用Base58编码,首先添加32位checksum(对这78字节数据进行两次SHA-256操作,然后取结果的前32位)然后进行Base58编码。最后得到一个长度112的字符。由于不同情况下选择的版本号不同,Base58格式表示的扩展密钥会有以下规则。在主网上以"xprv" or "xpub"开头,测试网上以"tprv" or "tpub"开头
请注意,父母的指纹只能作为检测软件中父母和孩子节点的快速方法,软件必须愿意处理冲突。在内部,可以使用完整的160位标识符。
导入序列化扩展公钥时,实现必须验证公钥数据中的X坐标是否与曲线上的点相对应。如果不是,则扩展公钥是无效的。
主密钥生成
可用扩展密钥对的总数大概是2512,但是生成的key长度只有256位,而且还要去掉一半不安全的密钥对。所以主密钥不是直接生成的,而是从一个短种子生成。
- 选择一个长度(128 bits到512 bits,建议使用256 bits)从一个随机数生成算法中生成一个种子序列S
- 计算 I = HMAC-SHA512(Key = "Bitcoin seed", Data = S)
- 将 I 分割成两个32byte的序列, IL and IR.
- k = parse256(IL) 作为主密钥, IR 作为主链码.
当 IL is 0 or ≥n, 主密钥无效.
规范:钱包结构
前面的部分指定了关键树和它们的节点。下一步是在这棵树上施加一个钱包结构。本节中定义的布局仅为默认设置,尽管客户被鼓励为了兼容性而模仿它,即使并非所有功能都受支持。
默认的钱包布局
一个HD钱包由多个“账号”构成。账号都有一个编码,默认账户account ("") 的编码是0.如果客户端不需要支持多个账户,那它们只需要使用默认账户就可以了。
每个账户都由两个密钥对组成:内部和外部。外部钥匙串用于生成新的公共地址,而内部钥匙串用于所有其他操作(更改地址,生成地址,...,任何不需要传送的任何内容)。不支持分离链码的客户端应该使用外部密钥对来处理一切事情。
- m / iH / 0 / k对应于从主m获得的HDW的帐号i的外部链的第k个密钥对。
- m / iH / 1 / k对应于从主m获得的HDW的帐号i的内部链的第k个密钥对。
用例
分享整个钱包:m
在两个系统都需要花费一个钱包的时候,它们之间需要分享主扩展私钥。节点可以为外部链保留一组N个观察keys,以监视收到的付款。观察内部链的代价是非常小的,因为这里不需要费用。对于第一个未使用的帐户的链条,可能会激活额外的预查 - 在使用时触发创建新帐户。请注意,帐户的名称仍需要手动输入,并且无法通过块链进行同步。
审计:N(m / *)
如果审计员需要完整访问收款和付款的清单,则可以共享所有帐户公用扩展密钥。这将允许审计人员在所有账户中查看所有来自钱包的交易,但不是一个秘密密钥。
查询每个办公室余额: m/iH
当一个企业有几个独立的办公室的时候,他们可以使用一个从主私钥生成的钱包。这将允许总部维持一个超级钱包,看到所有办公室的所有交易,甚至允许在办公室之间移动资金。
经常性的企业对企业交易:N(m / iH / 0)
如果两个商业伙伴经常转账,那么可以将特定账户的外部链(M / ih / 0)的扩展公钥用作“超级地址”,从而允许不能(容易)关联的频繁交易,但无需为每次付款申请新地址。这种机制也可以被挖掘池操作员用作可变支付地址。
不安全的收款人:N(m / iH/ 0)
当使用不安全的网络服务器来运行电子商务网站时,它需要知道用于接收付款的公共地址。网络服务器只需要知道单个账户外部链的公共扩展密钥。这意味着非法访问网络服务器的人最多可以看到所有收到的付款,但无法窃取资金,不会(平凡)能够区分即将离任的交易,也无法看到其他网络服务器收到的付款是几个。
兼容性
为了符合这个标准,客户必须至少能够导入一个扩展的公钥或私钥,以便将其直接后代作为钱包密钥来访问。本规范第二部分介绍的钱包结构(主账户/账户/链/子链)只是建议性的,但建议用作简单兼容性的最小结构 - 即使没有单独的账户或内部和外部链的区别。但是,对于特定的需求,实现可能会偏离它; 更复杂的应用程序可能需要更复杂的树结构。
安全
除了EC公钥密码本身的安全:
- 给一个公钥K,攻击者找到匹配的私钥最好的办法就是解决EC的离散对数问题(大概需要进行 2128 次操作).
该标准的预期安全属性是:
- 给一个子扩展私钥 (ki,ci) 和一个索引i,攻击者进行暴力破解需要将HMAC-SHA512执行2256 次
- 给任何一个数字 (2 ≤ N ≤ 232-1) 在 (索引, 扩展私钥) (ij,(kij,cij)), 不同的 ij's, determining 判断他们是否从同一个父扩展私钥推导出来 (既存在一个私钥扩展 (kpar,cpar) 使得在范围 (0..N-1) CKDpriv((kpar,cpar),ij)=(kij,cij)),
最好的办法是进行2256次HMAC-SHA512暴力计算
注意,以下属性不存在:
- 给一个扩展父公钥 (Kpar,cpar) 和一个子公钥 (Ki), 很难找到索引 i.
- 给一个父扩展公钥 (Kpar,cpar) 和一个普通子私钥 (ki), 很难找到父私钥 kpar.