Week 20 11.28~

11.28 HD Wallet

我们知道,在区块链中控制一个用户账户意味着知道该账户的私钥。而如果我们希望控制一批账户,则意味着我们需要知道每个账户的私钥。如果各个账户的私钥都是独立的,管理这批账户则会变得复杂而繁琐。一种解决方案是使用随机性足够高的随机数种子,从该随机数种子根据特定规则派生出私钥。这种情况下我们只需要知道随机数种子就能够控制一批账户了。

实现这一目的的手段并不唯一,目前被各类钱包软件/硬件广泛采用的标准为 BIP-32 提出的 HD Wallet ,意为分层确定性(Hierarchical Deterministic)钱包。其原理如下图所示,通过逐层派生,最终产生出实际使用的密钥。BIP-44 同时也提出了派生的标准:

m / purpose' / coin_type' / account' / change / address_index

一般通过改变coin_type生成用于不同区块链的私钥,如比特币中会采取coin_type为0,以太坊采取该值为60,Conflux中采取该值为503。

需要注意的是,为了保证HD Wallet的安全,除了需要保证 Master Seed(主随机数种子)不被泄漏外,还需要保证该随机数种子的熵足够高。BIP-32 中规定Master Seed需要在128bit至512bit之间。

derivation.png

11.29 助记词与HD Wallet

HD钱包标准被各类钱包软硬件采用,不过在实际的使用中,用户与开发者更多接触到的概念不是HD钱包的Master Seed(主随机数种子)而是“助记词”。例如当我们第一次使用fluent, metamask等浏览器钱包时,钱包会展示一组助记词并要求我们记下。那么助记词与HD钱包有什么关系呢?

助记词标准由BIP-39 提出。意在解决HD钱包的Master Seed的可读性差、誊写易出错等问题。就助记词的功能而言,助记词是用于产生(HD钱包)种子的种子PBKDF2(password-based key derivation funciton 2)是常用的从密码(口令)导出密钥的算法,常用于从密码(口令)导出进行加密的密钥,BIP39则使用该函数将助记词导出为HD钱包使用的Master Seed。该函数定义如下:

DK = PBKDF2(PRF, Password, Salt, c, dkLen)

该函数共有5个参数,BIP39中约定的各个参数为:

由上可见,只要我们知道助记词(和保护该助记词的口令),就能够通过算法唯一地导出HD Wallet的Master Seed。而相比HD钱包的Master Seed而言,助记词的可读性更好,誊写相对而言不易出错,从而提高了HD钱包的易用性。

11.30 助记词的产生与校验

BIP-39中对助记词的产生方式进行了说明。

nurse silk fiber machine jelly reduce coffee language fox forum cause team

由于每个单词有2048种选择,相当于每个单词都能编码11bit的信息,如第一个单词 nurse 对应 1212(二进制数为10010111100)。该助记词共12个单词,一个长为(12*11=)132bit 的二进制数对应。132位中,前128位需要是随机选取的,后4位为校验位,是前128位哈希值的前4位。

下面为产生一个助记词的完整流程:

  1. 生成长为ENTbit的随机数(ENT需要为32的整数倍且128<=ENT<=256),该随机数可以从操作系统获取,如使用python的os.urandom接口。
  2. 生成校验位。使用SHA256计算 ENT bit的哈希值,并截取前ENT/32位(ENT=128时则为4)。
  3. 将随机数与校验位拼接后,根据单词表进行编码。每11位可以编码得到一个单词,总共得到3/32*ENT个单词,即为助记词。

12.1 Keystore 文件

Keystore 文件是一种常见的用于存储私钥的文件格式。一般而言,Keystore文件并不直接存储私钥,而是存储着私钥加密后的密文以及加密的参数等信息,使用时需要通过口令(密码)解密才能获得原始的密钥。如下面是一个示例的keystore文件:

{
    "version": 3,
    "id": "db029583-f1bd-41cc-aeb5-b2ed5b33227b",
    "address": "1cad0b19bb29d4674531d6f115237e16afce377c",
    "crypto": {
        "ciphertext": "3198706577b0880234ecbb5233012a8ca0495bf2cfa2e45121b4f09434187aba",
        "cipherparams": {"iv": "a9a1f9565fd9831e669e8a9a0ec68818"},
        "cipher": "aes-128-ctr",
        "kdf": "scrypt",
        "kdfparams": {
            "dklen": 32,
            "salt": "3ce2d51bed702f2f31545be66fa73d1467d24686059776430df9508407b74231",
            "n": 8192,
            "r": 8,
            "p": 1,
        },
        "mac": "cf73832f328f3d5d1e0ec7b0f9c220facf951e8bba86c9f26e706d2df1e34890",
    },
}

其中ciphertext为加密后的密文,ciphercipherparams为加密时的参数。kdfkdfparams代表如何由口令得到加密使用的密钥。不过需要注意的是,即使keystore文件是经过加密的私钥文件,该文件也需要保证安全,尽量避免泄露。当加密密码设置得并不复杂时,攻击者可以通过暴力搜索的方式搜索可能的解密密码。

目前各类SDK均支持对Keystore文件的操作,读者可以阅读相关文档(如js-sdk)了解如何使用Keystore文件

12.2 如何安全地产生私钥?

私钥安全是区块链安全的重中之重。而安全地产生私钥则是私钥安全的首要条件。做到这一点需要选用熵足够高(即随机性足够高——足够长且难以预测)的种子作为产生私钥的随机源。典型的错误用法是直接使用各个语言提供的random接口,如python的random.random,java的java.util.random,这些接口提供的随机数是通过特定算法产生的伪随机数:其分布具备随机数的特性,能满足日常使用的需求,但是可预测非常高,因此不能用于产生私钥。再如wintermute的私钥被盗事件就是因为产生私钥的种子长度不足。

目前各个语言的SDK都内置了生成私钥的API,这些API都是如何获取随机性、产生私钥的呢?答案来自于操作系统。以Linux为例,系统会收集噪声数据,将产生的随机流置于/dev/urandom/dev/random中。通过读取文件中的数据,就能够获得来自操作系统的真随机数了。一般而言,在获取到足够长的真随机数后,SDK还会将该随机数与用户提供的随机性混合,进行一次哈希,最终得到私钥。python中的实现如下:

extra_key_bytes = text_if_str(to_bytes, extra_entropy)
key_bytes = keccak(os.urandom(32) + extra_key_bytes)

一般而言,SDK使用的随机数来自于/dev/urandom文件。urandom中的u意味着"unblock"(非阻塞),/dev/random会在操作系统提供的熵不足时阻塞,/dev/urandom则不会,这意味着特定情况下/dev/urandom能提供的随机性会低于/dev/random,但在绝大多数情况下,使用/dev/urandom已经足以满足产生私钥的安全性需求。如果确实对随机性有着严格的需求,可以参考以下代码,从/dev/random中获取随机性。

with open("/dev/random", 'rb') as f:
    print(f.read(32).hex())

需要注意的是,上述提到的要求不止适用于产生私钥,产生助记词等对随机性有要求的场景都是适用的。


Revision #10
Created 28 November 2022 03:28:23 by Darwin
Updated 2 December 2022 03:59:03 by Darwin