SM2密钥对生成:私钥与公钥的生成算法归纳总结
密钥对生成,说白了就是两个动作——随机数抽取和椭圆曲线点乘。很多人以为这事很简单,不就是调用个库吗?我在项目中见过好几次因为密钥对生成不规范导致的安全漏洞,最后排查发现是随机数质量不行或者没做边界检查。嗯,这章我们就抠开细节看看。
私钥生成:核心是随机数
SM2的私钥其实就是一个大整数,记作d,取值范围有严格规定:
d ∈ [1, n-2],其中n是SM2椭圆曲线的阶(推荐使用sm2p256v1时,n = 0xFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123)。
为什么不能取0、n-1或n?因为d=0会导致公钥为无穷远点,d=n或n-1对应的公钥分别是无穷远点或对称点,这些都不具备密码学意义。我曾经在测试环境里用了一条未检查的随机数生成器,结果跑出一对全零私钥……幸好是开发阶段,否则后果不堪设想。
我的个人经验:私钥生成必须搭配合格的随机数源。在服务端优先使用 /dev/urandom 或系统级DRBG;在嵌入式设备上要格外小心,我曾经碰过芯片硬件的硬件随机数发生器(TRNG)输出稳定度不足,导致多个设备生成了相同私钥。建议生成后做一次自相关校验。
私钥生成的流程大致如下:
- 调用安全随机数生成器获取一个长度为
ceil(log2(n))的字节串 - 将字节串转换为大整数 d
- 检查 d 是否在
[1, n-2]区间内 - 如果不在,重新生成(最多尝试若干次,防止死循环)
- 输出私钥 d
注意:不要直接用 random.randint 或 numpy.random 这类非密码学安全随机数生成器。国密标准要求至少使用符合 GM/T 0005-2012 的随机数生成模块。
公钥生成:椭圆曲线点乘运算
有了私钥d,公钥的计算公式非常简单:
P = d * G
其中G是SM2椭圆曲线的基点(base point),点乘运算是椭圆曲线上的标量乘法。
你想想看,d是整数,G是点,结果P也是曲线上的一个点。这个运算不可逆——从P反推d就是所谓的“椭圆曲线离散对数问题”,目前还没有多项式时间算法。这也是SM2安全性的基石。
在实现时,点乘通常采用“双倍-加”(Double-and-Add)算法,或者为了抵抗侧信道攻击,会使用蒙哥马利阶梯(Montgomery Ladder)。我早期做的一个SM2签名服务,起先用了简单的双倍-加,结果功耗分析工具一跑就能猜出密钥高位……后来改成蒙哥马利阶梯才通过国密认证。
# 伪代码:SM2点乘运算(使用蒙哥马利阶梯)
def point_multiply(d, G):
# 初始化两个点:R0 = O(无穷远点),R1 = G
R0 = O
R1 = G
# 从最高位到最低位扫描d的二进制
for bit in bits_of_d_from_msb_to_lsb:
if bit == 0:
R1 = add(R0, R1) # 点加
R0 = double(R0) # 倍点
else:
R0 = add(R0, R1)
R1 = double(R1)
return R0
公钥输出时,需要以未压缩格式或压缩格式进行编码:
- 未压缩格式:
04 || x坐标(32字节) || y坐标(32字节),共65字节 - 压缩格式:
02/03 || x坐标(32字节),共33字节(y坐标通过奇偶性恢复)
两者对比如下:
| 格式 | 标识字节 | 总长度 | 解码复杂度 | 常见场景 |
|---|---|---|---|---|
| 未压缩 | 0x04 | 65字节 | 低(直接读取xy) | 证书、密钥交换 |
| 压缩 | 0x02(y为偶)或0x03(y为奇) | 33字节 | 高(需计算平方根) | 存储空间受限设备 |
关键点:公钥必须属于椭圆曲线,即需要验证 y^2 ≡ x^3 + ax + b (mod p)。我在项目里发现过公钥数据被截断导致校验失败的情况,所以解码后建议做一次曲线方程验证。
算法校验与安全性检查
密钥对生成完毕后,不能直接用,需要过几道检查:
- 私钥非零且小于n-1: 上一步已经做了
- 公钥非无穷远点: 无穷远点不能用于签名/加密
- 公钥的坐标在模p范围内: 防止曲线攻击
- 公钥满足曲线方程: 确保是合法点
- 随机数质量验证: 可以做个简单连续重复测试(比如同一字节序列出现太多次则报警)
我曾经踩过的坑:有次上线前做非功能验证,发现一批SM2密钥对中有3个公钥完全一样。排查下来是Docker容器启动时熵池初始化太慢,导致前几个密钥用了相同种子。解决方案是在应用层加一个 os.urandom 预读取缓冲区,积累足够熵后再生成。
密钥对生成流程总览
下图把整个过程串起来了,你可以保存下来当检查清单:
整个流程下来,你其实只需要关注两个核心:随机数质量和点乘实现正确性。我个人习惯在生成后立刻做一个自检测——用新生成的私钥对一段固定数据签名,再用公钥验签,通过才算合格。这个额外步骤能暴露绝大多数实现错误。