07

X3DH 角色与消息流

3-Lane Sequence · 4 种密钥

IK / SPK / OTK / EK 各自解决什么 · 公私钥怎么分 · 4 个 DH 怎么算

🔑 密钥色板: IK 长期身份 SPK 中期 (含签名) OTK 一次性 EK 临时会话 Server (不可信) ^pub = 公钥可上行 ^priv = 私钥仅本地 Bob (接收方) Server (邮箱) Alice (发送方) ▶ Phase 1: Bob 注册时 / 周期性上传预密钥 本地生成 + 持有私钥 (IK_B^priv, IK_B^pub) · Identity (SPK_B^priv, SPK_B^pub) · 中期 [OTK_B¹..OTK_Bⁿ] · 一次性 池 upload 公钥仓库 (Bob's Bundle) { IK_B^pub, SPK_B^pub + sig, OTK pool: [otk¹^pub, otk²^pub, ...] } SPK 由 IK 签名以证身份 ▶ Phase 2: Alice 想给 Bob 发消息, 拉取 Bundle GET /bundle/bob Bob's Bundle 取出一个 OTK¹, 标记已用 收到 Bob 的公钥组合 { IK_B^pub, SPK_B^pub, OTK_B¹^pub } 注: 验证 SPK_B 的 IK_B 签名 ▶ Phase 3: Alice 生成临时密钥 EK, 算 4 次 DH, 发起初始消息 ① 本地生成临时密钥 (EK_A^priv, EK_A^pub) — 仅当前会话 ② 计算 4 次 DH DH₁ = DH(IK_A^priv, SPK_B^pub) DH₂ = DH(EK_A^priv, IK_B^pub) DH₃ = DH(EK_A^priv, SPK_B^pub) DH₄ = DH(EK_A^priv, OTK_B¹^pub) ③ SharedSec = KDF(DH₁ ‖ DH₂ ‖ DH₃ ‖ DH₄) → 用作 Double Ratchet 的初始 RootKey initial msg { IK_A^pub, EK_A^pub, otk_id, ciphertext } forward (Bob 收件箱) ▶ Phase 4: Bob 收到, 用对称的 4 个 DH 算出同样 SharedSec 从消息头取 IK_A^pub, EK_A^pub, otk_id DH₁ = DH(SPK_B^priv, IK_A^pub) ✓ DH₂ = DH(IK_B^priv, EK_A^pub) ✓ ... → 同样的 SharedSec ✓ 🔬 4 个 DH 各自混入了什么 / 解决了什么 DH₁ IK_A × SPK_B 混入 Alice 长期 + Bob 中期 → "认证 Alice 是 Alice" DH₂ EK_A × IK_B 混入 Alice 临时 + Bob 长期 → "认证 Bob 是 Bob" DH₃ EK_A × SPK_B 混入双方"中临"密钥 → 提供前向安全 (临时密钥用完即丢) DH₄ EK_A × OTK_B 混入"一次性" → 即使 SPK 长期不轮换, 每次会话也独立 (额外前向) Bob 用对称私钥执行同样 4 次 DH, 得到相同 SharedSec 最终 SharedSec = KDF(DH₁ ‖ DH₂ ‖ DH₃ ‖ DH₄) 缺任一 DH 都会破坏对应的安全属性 SharedSec → 喂给 Double Ratchet 作 RootKey 💡 X3DH = 身份认证 (IK) + 中期可用性 (SPK) + 前向安全 (OTK + EK) + 单次会话隔离 (EK), 4 个 DH 缺一不可
IK

Identity Key · 长期身份

  • • ECDSA / EdDSA / curve25519 长期密钥对
  • • 用户注册时一次性生成, 通常永不更换
  • • 用途: 验证身份 (我就是我), 给 SPK 签名
  • • 失去 IK = 身份被盗 (但不影响过去消息: 前向安全)
SPK

Signed PreKey · 中期

  • • 周期性轮换 (典型: 每周 / 每月)
  • • 由 IK 签名, 证明"是 Bob 同意的"
  • • 提供"OTK 用尽时的备用握手能力"
  • • 旧 SPK 保留一段时间以解密延迟到达消息
OTK

One-Time PreKey · 一次性

  • • 客户端预先批量生成 (e.g., 100 个), 上传 server
  • • server 每发出一个消耗一个
  • • 提供额外的前向安全 + 防 replay
  • • 需要客户端定期补充 (详见 #09)
EK

Ephemeral Key · 临时

  • • 发送方在每次会话首条消息时生成
  • • 公钥 EK_A^pub 随消息发出, 私钥用完丢弃
  • • 提供单次会话隔离 (跨会话不可关联)
  • • 区分 ^pub / ^priv 至关重要 (常见混淆点)
💡 一句话理解: X3DH = "三层密钥 + 一次握手"—— IK 验身份, SPK 保可用, OTK 加前向, EK 隔会话。 4 个 DH 计算把这 4 类密钥两两混合, 任一类被替换 / 缺失都会丢失对应的安全属性 — 这是为什么 Signal 偏要做"3DH+1"而不是简单的 1DH。

📚 基础知识速查 · Reference

不熟悉以下底层概念? 这里是 30 秒回顾。

签名EdDSA / Ed25519

椭圆曲线数字签名算法, Curve25519 上的签名版本。SPK 用 IK 签名, 证明"是该用户授权上传的"。

sig = Ed25519.sign(IK_priv, SPK_pub)

设计4-way DH 组合

4 次独立 DH 计算的 hash 拼接, 确保任何一方的密钥被替换都会改变 SharedSec。任一 DH 失效即整体失效。

SharedSec = HKDF(DH₁ ‖ DH₂ ‖ DH₃ ‖ DH₄)

数据结构PreKey Bundle

服务端发给请求方的"握手包"。包含发布方所有公钥 + SPK 的签名, 让发起方一次性获取 X3DH 所需输入。

{ IK^pub, SPK^pub, sig, OTK^pub }

命名公私钥语法约定

本图谱中用 ^pub / ^priv 上下标区分公私钥。这是常见的混淆点 — 务必记清楚谁参与 DH。

K^pub = 公钥可上行
K^priv = 私钥仅本地