4、子密钥生成算法:使用HMAC-SHA512进行父密钥到子密钥的派生
好,咱们进入实战环节最核心的一步——子密钥到底怎么算出来的?
前面我们聊了主密钥怎么从种子生成,也聊了链码的作用。但真正让证书链“动起来”的,是这套派生算法。说白了,就是给你一个父密钥,你怎么变出一个子密钥来。
我个人习惯把这一步叫做“单向跳转”。为什么强调单向?因为一旦派生出去,你就没法从子密钥倒推出父密钥。这是整个PKI体系的安全基石。
4.1 核心工具:HMAC-SHA512
HMAC-SHA512,名字听着挺唬人,其实就两样东西的组合:
- HMAC:带密钥的哈希运算。你可以把它理解成“加盐的哈希”。
- SHA-512:输出512位(64字节)的哈希函数。
为什么非要用HMAC,而不是直接哈希?我在项目中遇到过有人图省事,直接拿父密钥做SHA256。结果呢?安全性大打折扣。HMAC多了一层密钥保护,能有效防止长度扩展攻击。你想想看,做密码学的东西,省这一步往往就是埋雷。
关键公式:
I = HMAC-SHA512(Key = 父链码, Data = 0x00 || 父公钥 || 索引)
其中 I 是512位输出,前256位是IL(左半部分),后256位是IR(右半部分)。
4.2 派生步骤拆解
嗯,这里要注意,派生分两种情况:
- 普通派生:用父公钥 + 索引
- 强化派生:用父私钥 + 索引
咱们先讲普通派生,因为用得最多。
- 准备输入数据:把父公钥(33字节压缩格式)和索引(4字节,大端序)拼在一起。
- 执行HMAC-SHA512:用父链码作为HMAC的密钥,对上面的数据进行运算。
- 拆分结果:512位输出切成两半。左256位作为“子私钥的增量”,右256位作为“子链码”。
- 生成子私钥:子私钥 = (父私钥 + IL) mod n,n是椭圆曲线的阶。
- 生成子链码:子链码 = IR。
我曾经调试过一个Bug,子密钥死活对不上。查了两小时,发现是索引字节序搞反了。大端序和小端序,差一个字节,整个派生链就全断了。
4.3 代码示例:Go语言实现
光说不练假把式。我贴一段实际项目里用过的代码,你们感受一下:
func deriveChildKey(parentKey *hdkeychain.ExtendedKey, index uint32) (*hdkeychain.ExtendedKey, error) {
// 1. 准备数据:0x00 || 父公钥 || 索引(大端序)
data := make([]byte, 0, 37)
data = append(data, 0x00)
data = append(data, parentKey.PublicKey()...)
indexBytes := make([]byte, 4)
binary.BigEndian.PutUint32(indexBytes, index)
data = append(data, indexBytes...)
// 2. HMAC-SHA512
mac := hmac.New(sha512.New, parentKey.ChainCode())
mac.Write(data)
I := mac.Sum(nil)
// 3. 拆分
IL := I[:32]
IR := I[32:]
// 4. 计算子私钥
childPrivKey := new(big.Int).SetBytes(parentKey.Key)
ilInt := new(big.Int).SetBytes(IL)
childPrivKey.Add(childPrivKey, ilInt)
childPrivKey.Mod(childPrivKey, secp256k1.S256().N)
// 5. 组装子密钥
childKey := &hdkeychain.ExtendedKey{
Key: childPrivKey.Bytes(),
ChainCode: IR,
Depth: parentKey.Depth + 1,
ChildNum: index,
}
return childKey, nil
}
我的经验:生产环境中一定要校验子私钥是否为0或者大于等于曲线阶n。虽然概率极低,但一旦出现,需要跳过当前索引,用下一个索引重新派生。我在测试网遇到过两次,都是因为随机种子恰好生成了边界值。
4.4 派生路径与索引约定
索引不是随便填的。BIP32定义了一套约定:
| 索引范围 | 类型 | 说明 |
|---|---|---|
| 0x00000000 - 0x7FFFFFFF | 普通派生 | 使用父公钥,可公开派生 |
| 0x80000000 - 0xFFFFFFFF | 强化派生 | 使用父私钥,需要父私钥才能派生 |
强化派生用父私钥代替父公钥作为HMAC的输入。这样做的好处是:即使子链的公钥泄露,攻击者也推算不出父链的信息。我建议根密钥和中间CA密钥都用强化派生,叶子证书用普通派生就够了。
4.5 派生过程的SVG流程图
下面这张图把整个派生流程串起来了,建议保存下来对照着看:
4.6 避坑指南
做派生的时候,有几个坑我踩过,你们注意一下:
我曾经踩过的坑:
- 索引越界:索引最大到2^31-1(普通派生)或2^32-1(强化派生)。超过这个范围,派生结果无效。
- 子私钥为0:如果IL恰好使得子私钥等于0 mod n,这个子密钥是无效的。需要把索引加1重新派生。
- 链码长度:IR一定是32字节,不多不少。我见过有人把整个512位都当链码用,结果后面全乱套。
- 字节序:索引必须是大端序。小端序在x86机器上默认就是反的,记得用binary.BigEndian。
4.7 性能与安全权衡
HMAC-SHA512的计算开销其实不大。我做过基准测试,在普通服务器上每秒能跑几万次派生。但要注意:
- 强化派生因为要用父私钥,计算量一样,但安全性更高。
- 普通派生可以公开计算,适合在客户端做。
- 派生路径越长,累积的计算时间线性增长。100层派生大概也就几毫秒,不用担心。
我的建议:在实际项目中,把派生函数封装成独立模块,做好单元测试。尤其是边界情况(索引=0、索引=0x7FFFFFFF、索引=0x80000000)一定要覆盖到。我吃过亏,测试网跑得好好的,主网上线第一天就出了派生不匹配的问题,就是因为测试用例没写全。
好了,子密钥派生算法就这些。核心就一句话:HMAC-SHA512 + 拆分 + 模加。记住这个流程,后面讲多级证书链构建的时候,你会反复用到它。