跳转到内容

服务器身份固定

每台 rVPN 服务器都由一个长期存在的 Ed25519 身份密钥标识。客户端在首次连接 某个配置文件时会固定该密钥,并在固定的密钥发生变化且无有效轮换证明时拒绝连接。 这与 SSH 的信任模型相同,并额外引入了一个链式签名的轮换仪式,让运营方可以在 不影响现有客户端的前提下轮换密钥。


所有固定值都采用同一种规范编码:

ik:1:<52 个 base32 字符>
  • ik: — 命名空间,为未来密钥类型预留。
  • 1 — 版本号,合法轮换时可以明确地升级到 ik:2:…
  • 主体为 sha256(identity_key),采用 RFC 4648 base32、小写、无填充。 52 个字符,ASCII 安全,适合放入二维码。

示例:

ik:1:d4rgmp5b7ta6qmxi2mccwkjq4qxopfxzr7qivbfgu4wjycmuxnla

桌面版客户端曾经使用 32 个字符的十六进制指纹。它仍会在首次连接时读取旧值, 并在保存时以 ik:1: 形式重写 —— 无需修改任何配置。


在 iOS、macOS 和 Android 上,固定值存放在每个配置文件的 Server Fingerprint 字段中。不再有单独的 known_hosts.json

  • 首次连接Server Fingerprint 为空、Trust on First Use 打开时: 应用捕获服务器的固定值并写入该配置文件。
  • 后续连接:只有当服务器的固定值与已存值一致时,隧道才会建立。
  • 不一致:应用弹出 Server identity changed 对话框, 同时显示已固定和当次收到的两个固定值,并提供三个选项 —— 取消、 删除配置文件,或信任新身份。

若要在首次连接前显式固定身份,把运营方提供的固定值粘贴到配置文件编辑器的 Server Fingerprint 字段中。


桌面 rvpn 客户端把固定值存放在 known_hosts.json(路径可配置)中, 并在其上叠加一个配置层面的指纹字段。完整字段说明请参考 客户端配置[server_identity] 小节;要点:

  • fingerprint — 设为 ik:1:… 值可强制指定身份,无视 known_hosts.json 中的存值。
  • trust_on_first_use — 默认为 true。首次连接时捕获。
  • strict_mode — 默认为 false。设为 true 时会完全拒绝未知服务器; 必须在首次连接前设置 fingerprint
  • strict — 默认为 true。设为 false 时不一致仅记录警告而不中止连接。

客户端还会在保存 known_hosts.json 时把任何遗留的十六进制指纹迁移为规范的 ik:1: 格式,并保留原有的 first_seen 时间戳。


运营方轮换服务器身份密钥时,prekey bundle 会新增两个字段,用于让客户端 校验连续性:

  • identity_key_version — 单调递增;出现新身份即 +1
  • rotation_signature — 由身份对新公钥字节与新版本号进行的 Ed25519 签名。

已固定版本 N 的客户端遇到不一致时会:

  1. 看到某个 bundle 的身份与固定值不符,
  2. 校验 identity_key_version == N + 1
  3. 使用已固定的身份作为验证密钥,校验轮换签名,
  4. 两项通过则静默更新固定值为新值 —— 不弹对话框,连接照常进行。

若客户端遇到未签名的轮换或版本跳跃,则回落到上文的不一致对话框流程。

全新的服务器发布 v1 bundle,不带轮换签名:

Terminal window
rvpn-server keygen
rvpn-server prekey-bundle

轮换时,生成新身份并用旧密钥对新 bundle 签名:

Terminal window
# 把旧身份暂时保留在可访问的位置
mv server_identity.key old_identity.key
# 生成新身份
rvpn-server keygen --output server_identity.key
# 发布由旧身份签名的 v2 bundle
rvpn-server prekey-bundle \
--identity server_identity.key \
--output prekey-bundle.json \
--rotate-from old_identity.key \
--from-version 1

--rotate-from--from-version 必须成对出现。服务器不会发布未签名的 轮换。发布新 bundle 后,已固定客户端会静默更新,新客户端在首次使用时固定 新密钥。

请归档 old_identity.key —— 你只需要保留到发布轮换 bundle 为止, 但一旦销毁,就再也无法从该版本签署下一次轮换。


移动端和桌面端的配置文件导出都会携带 serverFingerprint 字段。当你导入的 .rvpn 配置文件已经带有固定值时,说明发送方通过带外方式为服务器身份做了 背书 —— 保存前请核对导入面板中显示的固定值与他们公布的值是否一致。

若导入的配置文件没有固定值,隧道会在首次成功连接时捕获一个,就和你在本地 新建配置文件时一样。


可防御

  • 首次成功连接之后的被动 MITM。
  • 恶意运营方接管你的服务器域名或 IP,并指向不同的后端。
  • 使用来自其他服务器的偷取到的 prekey bundle 进行重放。

无法防御

  • 首次连接时的主动 MITM。这是经典的 TOFU 局限。缓解方式:导入已经携带 固定值的配置文件,或在连接前通过带外渠道粘贴固定值。
  • 已越狱 / root 系统上对本地配置文件存储的篡改。原厂 iOS 会沙盒化配置文件; 原厂 Android 则将其保持为应用私有。

如果你确实需要主动替换服务器身份 —— 比如泄露后的恢复 —— 那么弹出的不一致 对话框就是正确的路径。通过带外渠道公布新固定值,告知用户接受变更, 他们就会带着新的固定值回到新密钥上。