比特币那些事(4)——钱包

概述

比特币中钱包并不是传统意义的钱包,它不包含比特币,仅仅包含密钥。每个用户都有一个包含多个密钥的钱包,钱包只包含私钥/公钥对的密钥链。用户用密钥签名交易,从而证明他们拥有交易输出,最终花费比特币。关于交易输出的概念,可以查看 《比特币那些事(2)——交易》

钱包

比特币钱包根据其包含的多个密钥是否相互关联,可以分为两种类型:

  • 非确定性钱包(Nondeterministic Wallet)
  • 确定性钱包(Deterministic Wallet)

非确定性钱包中的所有密钥都是由 随机数 独立生成的。密钥之间彼此无关,因此也称为 “Just a Bunch of Keys”,简称 JBOK 钱包

确定性钱包中的所有密钥都是从一个 主密钥 派生出来的。主密钥也称为 种子(Seed)。确定性钱包中所有密钥相互关联,如果有原始种子,则可以再次生成全部密钥。

非确定性钱包

在早期的比特币客户端(Bitcoin Core,也称比特币核心客户端)中,钱包只是随机生成的私钥集合。随机密钥的缺点就是如果你生成很多私钥,就必须保存它们所有的副本。每一个密钥都必须备份,否则一旦钱包不可访问时,钱包所控制的资金就付之东流。

确定性钱包

确定性钱包通过使用单项离散函数从公共的种子生成的私钥。种子是随机生成的数字。在确定性钱包中,种子可以恢复所有的已经生成的私钥,因此,只要在初始创建时对种子进行备份就可以了。

分层确定性钱包

确定性钱包使用了许多不同的密钥推导方法。最常用的推导方法是使用树状结构,称为 分层确定性钱包(Hierarchical Deterministic Wallet,简称 HD 钱包)。在 HD 钱包中,父密钥可以衍生出一系列子密钥,每个子密钥又可以衍生出一系列孙密钥,以此类推,无限衍生。

举例

下面,我们通过一个例子来介绍 HD 钱包的实现原理。

Alice 经营了一家网络商店销售T恤。她使用 Trezor 比特币硬件钱包(硬件 HD 钱包)来管理她的比特币。

Alice 首次使用 Trezor 时,设备从内置的硬件随机数生成器生成 助记词。钱包会在屏幕上按顺序逐个显示助记词。通过记下这些助记符,Alice 创建了一个备份,如下所示。

1 army 2 van 3 defense 4 carry 5 jealous 6 true
7 garbage 8 claim 9 echo 10 media 11 make 12 crunch

注:这里举例显示了 12 个助记词。事实上,大多数硬件钱包会生成更安全的 24 个助记词。

工作原理

HD 钱包的密钥推导主要包括以下几个步骤:

  • 创建助记词
  • 创建种子
  • 创建钱包

下面我们依次来介绍这个三个主要步骤。

创建助记词

BIP-39 是助记词行业标准,定义了助记词和种子的创建。助记词是由钱包使用 BIP-39 中定义的标准化过程自动生成的。助记词的生成主要包含以下这些步骤:

  1. 创建一个 128 至 256 位的随机序列(熵)
  2. 提取随机序列哈希值的前几位(随机序列长度/32),作为随机序列的校验和
  3. 将校验和拼接至随机序列的末尾
  4. 将序列进行分割成多个单元,每个单元占 11 位
  5. 将每个单元的值映射到一个包含 2048(2^11)个单词的字典
  6. 映射得到有顺序的单词组,即助记词

根据上述助记词的生成步骤,可以推测出随机序列(熵)与助记词长度的关系,如下表所示:

Entropy(bits) Checksum(bits) Entropy + Checksum(bits) Mnemonic Length(words)
128 4 132 12
160 5 165 15
192 6 198 18
224 7 231 21
256 8 264 24

创建种子

助记词创建之后,可以通过密钥延伸函数 PBKDF2 进一步生成种子。

密钥延伸函数 PBKDF2 有两个参数:助记词 (Salt)。盐的目的是为了增加暴力攻击的难度。

种子的生成主要包含以下步骤:

  1. PBKDF2 密钥延伸函数的第一个参数是助记词。
  2. PBKDF2 密钥延伸函数的第二个参数是盐。盐由助记词和可选的用户提供的密码组成
  3. PBKDF2 密钥延伸函数内部使用 HMAC-SHA512 算法,进行 2048 次哈希运算,生成一个 512 位的种子。

下表所示为示例的 128 位熵转换成 512 位种子的结果。 |Entropy(128 bits)|0c1e24e5917779d297e14d45f14e1a1a| |:---|:---| |Mnemonic(12 words)|army van defense carry jealous true garbage claim echo media make crunch| |Passphrase|(none)| |Seed(512 bits)|5b56c417303faa3fcba7e57400e120a0ca83ec5a4fc9ffba757fbe63fbd77a89a1a3be4c67196f57c39a88b76373733891bfaba16ed27a813ceed498804c0570|

创建钱包

钱包的创建主要包含以下几项工作:

  • 创建主私钥,即密钥树的根
  • 创建子私钥
  • 创建子公钥

下面我们依次介绍这几个步骤,在介绍完之后,我们再来看看其中存在的安全风险,进而介绍硬件子私钥的创建。

创建主私钥

HD 钱包的确定性源自于 根种子(Root Seed),即上述过程所生成的种子。

根种子通过 HMAC-SHA512 算法可生成 512 位哈希值。该将哈希值分成左右两部分,分别得到:

  • 主私钥(m)(Master Private Key(m)):主私钥(m)通过椭圆曲线算法可以生成 主公钥(M)(Master Public Key(M))。
  • 主链码(Master Chain Code)

创建子私钥

我们知道 HD 钱包采用树状结构进行密钥推导。在树状的密钥结构中,除了主密钥是通过根种子推导的,其他层级的密钥都是通过其母密钥推导的,采用 子密钥衍生函数 CKD (Child Key Derivation)。

子密钥衍生函数的调用需要三个参数:

  • 母私钥
  • 母链码
  • 索引号:32 位的值,因此每个母私钥可以推导出 2^32 个子私钥。

子私钥的具体衍生过程:根据母私钥推导出母公钥,将 母公钥-母链码-索引号 合并后使用 HMAC-SHA512 算法并结合 母私钥 生成 512 位哈希值。将该哈希值继续拆分成左右两部分,分别得到:

  • 子私钥
  • 子链码

上述子密钥衍生函数所使用的三个参数,其中母私钥和母链码的结合被称为 扩展私钥(Extended Private Key)。通过上述子私钥推导的原理,可以知道:一个扩展密钥作为 HD 钱包中密钥树的一个分支,可以衍生出该分支下的所有密钥。

扩展私钥 相对应是 扩展公钥(Extended Public Key),它由 母公钥母链码 组成,可用于通过母公钥直接创建子公钥。

创建子公钥

分层确定性钱包还有一个特点是:可以不通过私钥而直接从母公钥衍生出子公钥。这就给我们提供了两种衍生子公钥的方法:

  • 通过子私钥衍生子公钥
  • 通过母公钥衍生子公钥

子公钥的具体衍生过程(利用扩展公钥):将 母公钥-母链码-索引号 合并后使用 HMAC-SHA512 算法并结合 母公钥 生成 512 位哈希值。将该哈希值继续拆分成左右两部分,分别得到:

  • 子私钥
  • 子链码

这种子公钥的衍生过程不涉及任何私钥,运用到实际场景,可以实现私钥和公钥的分开管理。比如:在电商场景中,网络服务器仅维护公钥树结构,给每一笔交易创建一个比特币地址(只能接收比特币,而不能花费比特币)。为了安全起见,网络服务器不会有任何私钥。电商服务器则维护了私钥树结构,保证比特币的花费权在自己手上。

创建硬化子私钥

现在,我们深究一下上述子私钥和子公钥的创建方式,它们分别使用了 扩展私钥扩展公钥。这两种扩展密钥都包含了相同的母链码。这时候就可能存在安全风险:由于扩展公钥包含母链码,如果子私钥泄露了,攻击者就可以通过扩展公钥的母链码和子私钥组成一个扩展私钥。那么,该分支下的所有私钥都会泄露。更糟糕的是,子私钥与母链码可以用来推断母私钥。

为了应对这种风险,HD 钱包使用一种叫做 硬化衍生(Hardened Derivation)衍生函数。其本质就是让子私钥衍生和子公钥衍生使用不同的链码。

具体实现是 使用母私钥去推导子链码。非硬化子私钥衍生则是使用 母公钥去推导子链码

核心源码

如下所示为比特币开源库 BitcoinKit 中关于密钥推导的核心方法。从中,我们能一窥其技术原理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// BitcoinKitPrivateSwift.swift
// 子私钥和子公钥的衍生方法源代码
func derived(at childIndex: UInt32, hardened: Bool) -> _HDKey? {
var data = Data()
// 是否使用硬化衍生
if hardened {
data.append(0)
guard let privateKey = self.privateKey else {
return nil
}
// 强化衍生时,加入母私钥
data.append(privateKey)
} else {
// 非强化衍生,加入母公钥
data.append(publicKey)
}
var childIndex = CFSwapInt32HostToBig(hardened ? (0x80000000 as UInt32) | childIndex : childIndex)
// 加入索引号
data.append(Data(bytes: &childIndex, count: MemoryLayout<UInt32>.size))
// 结合母链码,生成哈希值。注意,是否为强化衍生将影响生成的链码
var digest = _Hash.hmacsha512(data, key: self.chainCode)
let derivedPrivateKey: [UInt8] = digest[0..<32].map { $0 } // 左半部分为私钥
let derivedChainCode: [UInt8] = digest[32..<64].map { $0 } // 右半部分为链码
var result: Data
if let privateKey = self.privateKey {
// 子私钥的衍生。调用本方法时会传入 privateKey
guard let ctx = secp256k1_context_create(UInt32(SECP256K1_CONTEXT_SIGN)) else {
return nil
}
defer { secp256k1_context_destroy(ctx) }
// 本质上,使用了母私钥衍生子私钥
var privateKeyBytes = privateKey.map { $0 }
var derivedPrivateKeyBytes = derivedPrivateKey.map { $0 }
if secp256k1_ec_privkey_tweak_add(ctx, &privateKeyBytes, &derivedPrivateKeyBytes) == 0 {
return nil
}
// 子私钥
result = Data(privateKeyBytes)
} else {
// 子公钥的衍生。调用本方法时不会传入 privateKey
guard let ctx = secp256k1_context_create(UInt32(SECP256K1_CONTEXT_VERIFY)) else {
return nil
}
defer { secp256k1_context_destroy(ctx) }
// 本质上,使用了母公钥衍生子公钥
let publicKeyBytes: [UInt8] = publicKey.map { $0 }
// 子公钥推导的特殊处理,结合了母公钥
var secpPubkey = secp256k1_pubkey()
if secp256k1_ec_pubkey_parse(ctx, &secpPubkey, publicKeyBytes, publicKeyBytes.count) == 0 {
return nil
}
if secp256k1_ec_pubkey_tweak_add(ctx, &secpPubkey, derivedPrivateKey) == 0 {
return nil
}
var compressedPublicKeyBytes = [UInt8](repeating: 0, count: 33)
var compressedPublicKeyBytesLen = 33
if secp256k1_ec_pubkey_serialize(ctx, &compressedPublicKeyBytes, &compressedPublicKeyBytesLen, &secpPubkey, UInt32(SECP256K1_EC_COMPRESSED)) == 0 {
return nil
}
// 子公钥
result = Data(compressedPublicKeyBytes)
}
let fingerPrint: UInt32 = _Hash.sha256ripemd160(publicKey).to(type: UInt32.self)
return _HDKey(privateKey: result, publicKey: result, chainCode: Data(derivedChainCode), depth: self.depth + 1, fingerprint: fingerPrint, childIndex: childIndex)
}

参考

  1. 《精通比特币》
  2. 《区块链技术指南》
  3. Mnemonic Code Converter
  4. BitcoinKit