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之间。
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中约定的各个参数为:
-
PRF
代表导出使用的伪随机函数。BIP-39使用HMAC-SHA512。 -
Password
代表导出使用的口令(密码)。PBKDF2原本用于从口令导出密钥,而BIP-39则是需要从助记词导出Master Seed,因此该值使用utf-8编码的助记词。 -
Salt
代表导出使用的盐值,调整该值可以改变导出的结果。BIP-39约定将该值默认设为mnemonic
。如果需要使用口令保护助记词(如口令为123456
),那么该值选为mnemonic
+口令(mnemonic123456
)。 -
c
代表迭代次数。BIP-39约定该值为2048. -
dkLen
代表输出值的比特数,BIP-39约定该值为512。
由上可见,只要我们知道助记词(和保护该助记词的口令),就能够通过算法唯一地导出HD Wallet的Master Seed。而相比HD钱包的Master Seed而言,助记词的可读性更好,誊写相对而言不易出错,从而提高了HD钱包的易用性。
11.30 助记词的产生与校验
BIP-39中对助记词的产生方式进行了说明。
-
助记词中可能出现的单词。单词表中挑选了具有辨识度且不易混淆的单词,共2048种,并按字母表顺序排列。
-
助记词的产生与校验。助记词可以认为是对二进制数的特殊编码,如下面的助记词包含12个单词:
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位。
下面为产生一个助记词的完整流程:
- 生成长为
ENT
bit的随机数(ENT需要为32的整数倍且128<=ENT<=256
),该随机数可以从操作系统获取,如使用python的os.urandom
接口。 - 生成校验位。使用SHA256计算
ENT
bit的哈希值,并截取前ENT/32
位(ENT=128
时则为4)。 - 将随机数与校验位拼接后,根据单词表进行编码。每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
为加密后的密文,cipher
与cipherparams
为加密时的参数。kdf
与kdfparams
代表如何由口令得到加密使用的密钥。不过需要注意的是,即使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())
需要注意的是,上述提到的要求不止适用于产生私钥,产生助记词等对随机性有要求的场景都是适用的。
No Comments