3. 密钥生成原理:从随机种子到签名密钥对

密钥生成,是整个 Ed25519 签名体系的起点。很多人觉得这步就是调个库函数完事,但我在嵌入式移植中踩过不少坑——随机数生成器不够安全、种子管理不当、公钥推导过程理解有偏差,这些都会让后续的签名验证形同虚设。今天咱们就把这层窗户纸捅破。

3.1 私钥生成机制:种子才是真正的秘密

Ed25519 的私钥,其实不是直接生成的。它背后有一个更核心的东西——种子(seed)。

标准流程是这样的:

  1. 系统生成一个 256 位(32 字节)的安全随机数,这就是种子。
  2. 对种子做 SHA-512 哈希,得到 64 字节的哈希值。
  3. 取哈希值的前 32 字节,经过“削剪”处理(后面细说),得到私钥 sk。
  4. 取哈希值的后 32 字节,作为推导公钥时的“非对称密钥前缀”。

说白了,种子就是私钥的私钥。你只要保管好种子,随时可以恢复出完整的密钥对。我在一个 IoT 项目中,设备出厂时只烧录种子,私钥和公钥都在首次上电时现场推导——这样即使固件被 dump,攻击者也拿不到明文私钥。

核心结论:Ed25519 的“私钥”是可推导的,真正的秘密是那 32 字节的种子。

3.2 公钥推导过程:点乘运算的艺术

有了私钥,公钥怎么来?一句话:私钥乘以椭圆曲线的基点。

具体步骤:

  • 私钥 sk 是一个 32 字节的标量(经过削剪后的小端整数)。
  • 基点 B 是 Ed25519 曲线上的固定点,坐标是公开的。
  • 公钥 pk = sk * B,这是椭圆曲线上的标量乘法(点乘)。

这里有个细节——削剪(clamping)。私钥的前 32 字节不能直接用,得做三步操作:

  1. 将最低 3 位清零(确保是 8 的倍数,抵抗小群攻击)。
  2. 将最高位清零(确保标量小于曲线阶)。
  3. 将次高位设为 1(防止非素数阶子群攻击)。

嗯,这些设计都是有密码学依据的。我曾经在移植时忘了做削剪,结果签名验证时好时坏,查了两天才发现是这里出了问题。

// 伪代码:Ed25519 公钥推导
func derive_public_key(seed [32]byte) [32]byte {
    hash := sha512(seed)
    // 前 32 字节做削剪
    sk := clamp(hash[0:32])
    // 后 32 字节是前缀
    prefix := hash[32:64]
    // 点乘:pk = sk * B
    pk := scalar_mult(sk, BASE_POINT)
    return pk
}

3.3 种子与密钥对的关系:一棵树上的果实

种子、私钥、公钥三者的关系,可以用一张图说清楚。

Ed25519 密钥生成流程 种子 (32 字节) SHA-512 前 32 字节 削剪 (Clamping) 后 32 字节 (前缀) 私钥 sk (32 字节) 点乘: sk * B 公钥 pk (32 字节) ⚠️ 关键点 种子是唯一需要保管的秘密。 私钥和公钥均可从种子推导。 丢失种子 = 丢失一切。

我的经验:在嵌入式设备中,我习惯把种子存储在安全元件(SE)或 eFuse 中,私钥和公钥只在 RAM 中临时存在。这样即使设备被物理破解,攻击者也拿不到持久化的私钥。

3.4 安全随机数生成:密钥安全的基石

种子是随机生成的。如果随机数生成器(RNG)不够安全,整个密码体系就是纸糊的。

Ed25519 标准要求使用密码学安全伪随机数生成器(CSPRNG)。常见的选项:

平台 推荐 API 说明
Linux /dev/urandom 或 getrandom() 内核维护的熵池,足够安全
MCU (ARM Cortex) 硬件 RNG 外设 + 软件去偏 需要验证硬件 RNG 是否符合 SP 800-90B
ESP32 esp_random() 基于硬件噪声,但建议混合软件熵
STM32 RNG 外设 + 自检 注意某些型号 RNG 有已知缺陷

⚠️ 避坑指南:我曾经在一个项目中使用 MCU 内置的伪随机数生成器(PRNG)来生成种子,结果发现每次上电后的前几个随机数竟然是一样的——因为 PRNG 的种子是固定的。从那以后,我坚持使用硬件 RNG 或混合多路熵源。

在嵌入式环境中,我推荐一种“混合熵”策略:

  1. 从硬件 RNG 读取 32 字节。
  2. 混入芯片唯一 ID(如 MCU 的 UID)。
  3. 混入一个高精度定时器的低几位(如 SysTick 的纳秒级抖动)。
  4. 对混合结果做一次 SHA-256,得到最终种子。

这样做的好处是:即使硬件 RNG 有缺陷,其他熵源也能弥补。说白了,就是不要把鸡蛋放在一个篮子里。

3.5 密钥对的一致性验证

生成完密钥对,怎么确认它们是一对?我教你一个简单的自检方法:

// 自检:验证密钥对一致性
func verify_keypair(seed [32]byte) bool {
    pk1 := derive_public_key(seed)
    // 重新推导一次,看公钥是否一致
    pk2 := derive_public_key(seed)
    return pk1 == pk2
}

如果返回 false,说明推导过程中有随机性介入——那你的实现就有 bug。正常的 Ed25519 实现,给定同一个种子,永远得到同一个公钥。

记住:确定性推导是 Ed25519 的重要特性。它意味着你可以从种子恢复整个密钥对,不需要额外存储私钥和公钥。

嗯,密钥生成原理就讲到这里。种子是根,私钥是干,公钥是果——理解了这三者的关系,后面的签名和验证就水到渠成了。