比特币那些事(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 中关于密钥推导的核心方法。从中,我们能一窥其技术原理。

// 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
我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章