relay-rs / 安全笔记

relay-rs 加密与密钥生命周期笔记

面向一个小型二层中继实验,梳理密码学词汇、信任边界、生命周期风险与成熟度层级。

记录日期:2026-05-30 实现边界校准:2026-07-26 原始资料:英文 Markdown 原文长度:517 行
历史阅读副本。这是一份工程笔记,不是密码学证明,也不是当前实现的事实源。
目录

日期:2026-05-30

背景:

  • 项目:小型二层中继实验。
  • 当前加密形态:Noise_NK_25519_ChaChaPoly_BLAKE2s
  • 当前未使用 Ed25519;id_x25519 是 Noise responder 的长期 static DH 密钥。
  • 承担 secure / UDP listener 的节点必须持有 id_x25519 私钥;纯 active connector 只需要对端公钥和授权 token。
  • 每次连接握手会派生 session / transport key。
  • 在 center-issued 模式下,中心签发 pair-scoped auth secret,但不直接签发数据面加密密钥。
  • 尚未实现:运行期间定期 rekey、节点吊销、长期密钥轮换、密钥泄露响应。

本文面向后端工程师,不是密码学证明。目标是把工程边界说清楚。

01

心智模型

系统分为两个平面,但数据面的信任边界还要继续按实际路径拆开:

控制面:
  身份、授权、Peer 发现、公钥分发、策略

UDP direct:
  Edge A <-- Noise --> Edge B
  C 负责协调,不参与该 Noise transport,不持有 transport key,也不处理明文帧

secure TCP fallback:
  Edge A <-- Noise --> C <-- Noise --> Edge B
  C 终止两段安全通道,解密并路由明文 Ethernet frame

因此,“中心不直接知道会话的数据面加密密钥”只适用于 UDP direct。此时 C 是协调者;但如果 C 可以静默替换 Peer 公钥,它仍位于 direct 路径的身份与 MITM 信任边界内。

secure TCP fallback 是逐段加密,不是 Edge A 到 Edge B 的端到端加密。C 持有两侧链路各自的 transport key,并处理两侧之间的明文帧,因此天然位于 fallback 数据机密性的信任边界内。

02

ECDH、Noise、TLS、WireGuard 与 Tailscale

ECDH

ECDH 是基础构件:双方分别把自己的私钥与对方的公钥组合,派生出相同的共享秘密。

本项目里的 25519 指 X25519 风格的 Diffie-Hellman。它只能给出共享秘密,不是一套完整协议。

单独使用 ECDH 回答不了:

  • 对方究竟是谁?
  • 这把密钥是不是新鲜的?
  • 是否发生了重放?
  • 具体应该使用哪些加密密钥?
  • 握手 transcript 的哪些内容得到了认证?

这些属于协议责任。

Noise

Noise 是安全信道协议框架。它用 DH、hash/KDF 和 AEAD 加密组织握手。Noise pattern 规定交换哪些 static / ephemeral 公钥,以及哪些 DH 计算被混入握手 transcript。

对于 Noise_NK

N:发起方不在 Noise pattern 中发送或使用 static identity key
K:发起方事先知道响应方的 static public key

如果发起方事先拿到的是正确的响应方静态公钥,那么响应方可以向发起方证明身份。但普通 NK 不会通过 Noise static key 认证发起方;发起方授权必须由应用层完成,例如 pair-scoped auth secret 或签名 payload。

这对 relay-rs 很重要:

如果响应方公钥已经被 pin 或通过其他方式可信,Noise_NK 可以认证响应方。
它不会自动证明发起方的长期身份。

TLS 1.3

TLS 1.3 是标准化的安全传输协议。它通常使用证书认证服务器,并通过临时密钥交换获得前向保密。它还定义了完整的 key schedule、session resumption、key update、alert、transcript binding,并拥有广泛的互操作生态。

这里暂时不继续拆算法名,只记住各自回答的问题:

身份签名(Ed25519 / ECDSA / RSA):
  证明“本次握手确实来自预期的服务器或设备”

密钥协商(X25519 / ECDHE):
  让通信双方各自在本地算出本次连接使用的共同秘密

对称传输加密:
  使用握手派生的短期 transport key 加密后续数据

allowlist / policy:
  即使身份已经确认,仍要决定它是否获准接入、能和谁通信

早期 TLS 的 RSA key transport 会用服务器 RSA 公钥加密客户端生成的 pre-master secret;现代 TLS 1.3 则把身份签名和临时密钥协商明确分开。对理解 relay-rs 而言,记住这层职责分工已经足够。

TLS 范围更广、更通用;Noise 更小,也更适合协议设计者组合。WireGuard 选择 Noise 而不是 TLS,是因为它需要紧凑、固定用途的 VPN 协议。

WireGuard

WireGuard 是建立在特定 Noise 握手、Curve25519、ChaCha20-Poly1305、BLAKE2s 和一组固定选择之上的 VPN 协议。它使用长期公钥表示节点身份,在握手期间派生对称 transport key,并定期刷新。

WireGuard 不只是 ECDH。它还包含身份、transcript binding、重放处理、定时器、计数器、密钥生命周期规则和数据包格式。

Tailscale

Tailscale 用 WireGuard 承载加密数据面,并增加协调与控制面:

  • 设备登录与身份;
  • 公钥分发;
  • ACL / policy;
  • NAT 穿透;
  • Peer endpoint 发现;
  • 密钥过期与设备管理。

Tailscale 带来的工程启发:

控制面可以分发公钥和策略,而不直接承载数据面明文。
但是,如果 endpoint 无条件信任控制面提供的公钥,控制面仍然处在身份信任边界内。
03

密钥、身份与 Token

Identity Key

代表一个节点的长期密钥对。

例如:

edge.id_x25519_private
edge.id_x25519_public

可用于:

  • 稳定的节点身份;
  • Peer allowlist;
  • 公钥 pinning;
  • 签名或 static DH,具体取决于协议设计。

relay-rs 当前没有使用 Ed25519 签名。id_x25519 具体承担的是 Noise responder static DH key,并通过对端预置或 pin 的公钥表达 listener identity。只有承担 secure / UDP listener 的节点需要本地私钥;纯 active connector 不发送 static identity key,也不需要本地 id_x25519 私钥。

这对应当前部署规则:docs/security-design.md:174 只要求 listener 部署 server private key,纯 encrypted active connector 只持有对端公钥和 token。

风险:

  • 私钥泄露意味着节点身份被攻破;
  • 如果不支持轮换,恢复过程会非常手工且痛苦。

Session Key / Transport Key

握手派生出来的短期对称密钥,用于加密和认证数据面数据包。

可用于:

  • 实际加密流量;
  • 两个方向各自的 transport encryption。

基本规则:

  • 不要持久化;
  • 不要写入日志;
  • 会话结束后清除;
  • 在 nonce 或生命周期接近风险边界之前,通过重新握手或 rekey 替换。

Auth Token

Bearer credential,用于证明调用方获准执行某项控制面操作。

例如:

  • 注册节点;
  • 获取 Peer 列表;
  • 请求中心为一对节点授权;
  • 上传 endpoint 信息。

Auth token 通常不是加密密钥。Bearer token 被盗后,攻击者可以在它过期或被吊销前冒充持有者执行操作。

Token 与 Ed25519:认证和授权分开看

假设未来设备使用 Ed25519 证明身份,变化的主要是“设备怎样证明自己”,不是“服务端是否还需要名单和策略”。

层次 当前 token 模式 假设的 Ed25519 模式
身份凭据 谁持有 token,谁就能自称对应 label 谁能完成本次签名,谁就持有对应设备私钥
服务端登记 token hash -> label DID -> public key -> status
认证动作 在加密通道内提交 bearer token 对本次 challenge 或握手 transcript 签名
授权动作 allowlist 判断 token 是否获准 registry / policy 仍要判断该 DID 是否获准
泄露影响 token 被复制后即可被直接重用 复制公钥或证书本身不能冒充设备;私钥泄露会允许冒充,注册表或签发体系失陷则是另一类信任边界故障

所以 Ed25519 不会自动消灭 allowlist。它可以把名单从“哪些 bearer token 可以进入”变成“哪些设备公钥对应的身份可以进入,以及进入后拥有什么权限”。

设备身份从生产到每次连接

最小模型不要求先引入证书。每台设备拥有不同的 Ed25519 密钥对,服务端保存公钥和设备状态即可:

生产或首次注册:
  设备 D 在本地生成,或由受控生产流程安全注入 private_D 和 public_D
  private_D 留在设备内,不上传
  服务端登记 DID-0001 -> public_D -> enabled

每次连接:
  服务端生成本次 challenge
  D 返回 DID-0001 和 Sign(private_D, challenge + 本次会话信息)
  服务端查到 public_D,验证签名,再查询 DID-0001 的权限
材料 设备侧 服务端 是否必须保密
Ed25519 私钥 本地生成或安全注入,并长期保存 不保存
Ed25519 公钥 可保存并对外提供 登记到设备注册表
DID 作为设备标识 映射公钥、状态和权限 通常否
Challenge 只签署本次收到的值 每次新生成并校验 否,但必须保持新鲜
权限与吊销状态 可以不知道完整策略 判断 enabled / disabled / scope 属于策略数据

每台设备应使用不同私钥。多台设备共用一把私钥,会退化成“泄露一台即可冒充整批设备”,也无法单独吊销。规模较小时可以直接维护公钥注册表;规模较大时,证书可以证明“某个 DID 与某个公钥由制造商认可”,但服务端仍需要设备状态、权限和吊销记录。

Token 也不必彻底消失。一种自然分工是:一次性生产 token 只负责首次登记 DID -> public key,此后每次连接使用设备私钥签名,注册表负责持续授权。首次登记仍要验证设备确实持有待登记公钥对应的私钥,并原子地核销生产 token;否则拿到 token 的人可以抢先绑定自己的公钥。

Pair Secret

仅适用于一对节点或一次会话意图的对称 secret。

可用于:

  • 证明 A 与 B 已经获得中心授权;
  • 把授权绑定到节点对、时间窗口或 nonce;
  • 在具备 TTL 和 transcript binding 时限制重放。

不要把它当成:

  • 长期节点身份;
  • 公钥验证的替代品;
  • 数据面加密密钥,除非协议明确这样设计。

在本项目中,center-issued pair secret 更适合理解为授权材料,而不是数据面密钥。

当前实现中的具体流程是:

C 为 A-D 生成随机 32 字节 S_AD
C 通过各自已有的 secure TCP 控制通道,把同一个 S_AD 发给 A 和 D
C 同时协调 candidate、Peer public key、pair_id 和 generation

A 与 D 直接进行 Noise NK 握手
  - 被选为 responder 的节点使用本地 static private key
  - initiator 使用临时密钥,并 pin responder public key
  - 握手派生两个方向的 transport key
  - C 不参与这次 key schedule,也拿不到 transport key

假设本次由 A 发起、D 响应,Noise 通道建立后:
  A -- Encrypt_transport(Auth { S_AD }) --> D
  D 解密后,在 CenterIssuedPairAuthRegistry 中核对 S_AD

S_AD 是 C 知道的 pair 通行证,不是 A 或 D 的私钥,也不进入当前 Noise key schedule。当前代码在 C 启动时按 center-issued pair 生成它,并把它绑定到本次 pair policy generation;它不是每条连接重新生成的 secret,目前也没有独立 TTL 或运行期间定期轮换。

因此,C 仅凭抓到的 direct 报文、公钥和 S_AD,不能还原已经建立的 A-D transport key。但 C 是通行证签发者,又控制 candidate 和公钥分发;被攻破的 C 可以签发新材料或诱导建立新会话。当前模型是“可信中心协调下的 direct 加密”,不是对中心零信任。

Public Key Pinning

记住或预先配置某个 Peer 应有的公钥。

可用于:

  • 阻止公钥被静默替换;
  • 减少对中心的信任;
  • 发现“同一个 node id,公钥却变了”的事件。

Pinning 回答的是:

这是我原本期望 Peer B 使用的公钥吗?

它回答不了:

Peer B 现在仍被授权吗?
这个节点健康吗?
这条路由应该被允许吗?

后面这些属于策略问题。

04

生命周期术语

Forward Secrecy

前向保密表示:即使长期 identity key 以后泄露,只要握手使用了新鲜的临时秘密,并且这些 ephemeral / session state 已被清除,过去录下的会话仍应保持机密。

对于 relay-rs:

  • 使用新鲜 ephemeral key 的 Noise 握手可以为会话流量提供前向保密;
  • 前提是确实使用新鲜密钥,并删除 ephemeral / session state;
  • 长期密钥泄露后,前向保密并不能保护未来会话。

Rekey

Rekey 是替换活跃连接的 transport key。

原因包括:

  • 缩小 session key 泄露后的影响;
  • 避免 nonce 耗尽;
  • 限制单把密钥承载的流量;
  • 遵守按时间定义的密钥寿命。

常见方式:

  • 重新执行握手并切换 transport key;
  • 使用协议内置的 key update。

对实验项目而言,断开重连并重新握手可以接受;对产品而言,密钥寿命规则应该明确。

Key Rotation

Key rotation 是替换长期身份密钥或控制面授权密钥。

例如:

  • 节点生成新的 id_x25519
  • 服务器轮换用于签名 node list 的密钥;
  • 更换 auth token signing key;
  • 更换 pair secret derivation key。

Rotation 与 rekey 不是一回事:

rekey:替换 session / transport key
rotation:替换长期 identity / control-plane key

Node Revocation

节点吊销表示撤销一个节点参与系统的权利。

它需要:

  • 节点注册表;
  • 分发吊销状态的机制;
  • Peer 侧执行;
  • 明确如何处理已经建立的会话。

一个难点:

节点 X 被吊销时,涉及 X 的现有会话立即终止,
还是只禁止建立新会话?

内部工具通常更适合立即断开。

Compromised-Key Response

这是“某把密钥泄露了”之后的运行手册。

它应该回答:

  • 泄露的是哪类密钥?
  • identity key、auth token、pair secret、session key,还是中心签名密钥?
  • 攻击者可以做什么?
  • 必须吊销什么?
  • 必须轮换什么?
  • 哪些会话必须终止?
  • 哪些日志可以证明事故窗口?
  • 如何强制 Peer 获取新的信任状态?

没有这份运行手册,吊销和轮换都只能算局部功能。

05

relay-rs 当前边界

如果边界表达清楚,当前设计作为实验项目是可以接受的:

  • Noise 为每条实际安全连接派生 transport key;
  • UDP direct 的 Noise 会话建立在两个边缘节点之间,C 不持有该会话的 transport key,也不处理其明文数据面;
  • secure TCP fallback 由两条 edge↔C Noise 链路组成,C 终止两侧通道并处理明文 Ethernet frame;
  • 中心不直接签发数据面密钥,但这不等于 fallback 对中心不可见;
  • Pair-scoped secret 属于授权材料;
  • 尚无完整密钥生命周期;
  • 尚无自动节点吊销;
  • 尚无密钥泄露响应;
  • 不承诺达到开放产品级安全。

关键是不要过度声称。

可以这样描述项目:

relay-rs 使用 Noise_NK 为每条安全连接派生 transport key。
UDP direct 在两个边缘节点之间建立 Noise 会话,C 不持有该会话密钥。
secure TCP fallback 在 edge 与 C 之间逐段终止,C 可以看到并路由明文帧。
中心签发的 pair secret 用于授权节点对,不是数据面加密密钥。
当前生命周期支持仍属实验性质:没有运行期间定期 rekey,
没有产品级节点吊销,也没有自动化的密钥泄露响应。
06

容易混淆的地方

UDP direct:“C 转发公钥”与“C 可以 MITM”

对 UDP direct 而言,如果中心 C 只转发公钥,而且 endpoint 已经知道或 pin 住正确的 Peer 公钥,那么 C 无法在不被发现的情况下对这条端到端数据面做 MITM。

但是,如果 endpoint 无条件相信 C 当天提供的任何公钥,C 就可以替换公钥:

A 以为自己在和 B 通信,实际使用的是 C 的公钥。
B 以为自己在和 A 通信,实际使用的是 C 的公钥。
C 分别终止两条加密会话。

C 能否做到这一点,取决于公钥信任方式:

相对安全:
  Peer key 已经 pin、由 C 无法伪造的密钥签名,
  或者通过带外方式验证

不安全:
  Peer key 就是 C 今天告诉我的那个 key

secure TCP fallback:链路加密不等于端到端加密

fallback 不需要 C 冒充任何 Peer。Edge 与 C 分别完成 Noise 握手;C 收到密文后先解密,再把明文 Ethernet frame 交给 RelayCore 路由,并通过另一条 secure TCP 连接重新加密发送。

线上看到的是两段密文:Edge A <--> C、C <--> Edge B
C 内部处理的是明文 Ethernet frame

当前代码依据:src/secure.rs:194 在 C 上把 Noise ciphertext 解成 plaintext;src/secure.rs:525 随后解析明文 Ethernet frame,并把它交给转发事件流。

所以 C 位于 fallback 的数据机密性信任边界内。这是当前路径设计,不是单纯的公钥分发风险。

“中心不签发数据面密钥”对 UDP direct 必要,但并不充分

对 UDP direct 而言,中心不签发 session key 是好事。但是,如果中心能够替换身份,它仍可以诱导 endpoint 与中心派生密钥,从而成为中间人。

真正的问题是:

中心能否让 A 把 C 的公钥当成 B 的公钥接受?

如果可以,中心就在 MITM 信任边界里。

Pair Secret 不等于 Encryption Key

Pair secret 可以授权一次连接。除非协议明确把它混入握手 key schedule,否则不应该把它描述为“加密流量所用的密钥”。

如果 pair secret 在 session key 已经存在以后,作为 Noise 加密 payload 发送,它是在加密信道内证明授权;如果作为 PSK 混入握手,它会改变协议的密码学属性。这是两种不同设计。

Identity Key 不等于 Session Key

长期 identity key:

  • 稳定;
  • 代表节点;
  • 轮换成本高;
  • 泄露影响范围大。

Session key:

  • 短期;
  • 加密一次连接;
  • 应该被清除;
  • 如果具备 rekey / lifetime 约束,泄露影响可以被限制。

Auth Token 不等于节点身份

Token 可以允许一个节点向中心请求某些操作,但它不应该成为数据面 Peer 确实是预期节点的唯一证明。

07

成熟度层级

Level 1:实验项目

目标:学习、验证架构,避免明显的自我欺骗。

应该具备:

  • 每次连接使用新的 Noise 握手;
  • 不记录 private key / session key;
  • 清楚区分 identity key、pair secret 与 session key;
  • 基本的公钥 allowlist 或人工 pinning;
  • Pair secret 尽可能限制到节点对并设置短 TTL;
  • 在可行范围内检查重放与 freshness;
  • 记录已经接受的风险;
  • 测试错误公钥、错误 pair secret、过期 pair secret 和握手失败。

可以后置:

  • 自动节点吊销;
  • 完整的密钥轮换体验;
  • 无需重连的在线 rekey;
  • 事故响应自动化。

Level 2:内部工具

目标:足以供小型可信团队或内部网络使用。

应该增加:

  • 带显式 enable / disable 状态的节点注册表;
  • Peer public key pinning 或签名 node map;
  • Pair secret 的 TTL、节点对范围和 nonce / challenge binding;
  • 定期重新握手或连接寿命上限;
  • 节点吊销既阻止新会话,也在需要时终止现有会话;
  • 基本密钥轮换流程;
  • 认证、Peer 映射、部署和握手失败的审计日志;
  • 握手失败、授权失败、rekey / reconnect 和活跃会话指标;
  • 身份材料备份与恢复,或者明确决定不备份;
  • 密钥泄露响应检查表。

仍可后置:

  • 完善的用户恢复流程;
  • 外部安全审查;
  • 复杂多租户策略。

Level 3:开放产品

目标:让原始信任圈之外的用户可以依赖它。

应该增加:

  • 明确的威胁模型;
  • 签名控制面状态;
  • 在可能范围内防御恶意或已被攻破的协调服务器;
  • 公钥 pinning、带告警的 TOFU,或类似 tailnet lock 的审批机制;
  • 真实的吊销传播语义;
  • 长期密钥轮换体验;
  • 按时间或流量定义的 session rekey 策略;
  • 兼容性和版本演进方案;
  • 安全存储集成;
  • rate limiting 与 DoS 边界;
  • 结构化审计轨迹;
  • 事故响应文档;
  • 针对数据包解析和状态机的 fuzzing / property test;
  • 在安全主张进入宣传材料前完成独立审查。
08

relay-rs 实际建议

近期不要试图变成 WireGuard 或 Tailscale。更有价值的下一步,是把当前信任边界表达清楚。

建议任务:

  1. 按数据路径准确记录中心 C 的职责:

    UDP direct:协调连接,不持有 edge↔edge transport key,不处理明文帧
    secure TCP fallback:终止两侧 Noise 通道,处理并路由明文帧
  2. 增加 Peer 身份表:

    peer_id -> expected id_x25519_public -> status
  3. 把 pair secret 绑定到:

    initiator_id, responder_id, initiator_pub, responder_pub,
    issued_at, expires_at, nonce
  4. 决定 pair secret 是:

    加密 payload 内的授权材料

    还是:

    作为 PSK 混入 Noise

    不要把两者描述成同一回事。

  5. 即使通过重连实现,也要设置最大会话寿命:

    运行 N 分钟或传输 M 字节后关闭并重新握手
  6. 增加人工吊销:

    disabled 节点不能建立新会话
    观察到吊销后关闭现有会话
  7. 编写 SECURITY.md,明确当前 non-goal。

当前适合声明的 non-goal:

本项目尚未提供产品级密钥轮换、自动节点吊销或密钥泄露恢复。
中心签发的 pair secret 用于授权节点对,不直接作为数据面加密密钥。
UDP direct 对 C 隐藏数据面明文;secure TCP fallback 不提供对 C 的机密性。
当前信任模型假设 Peer 公钥来自可信来源或已经被 pin。
09

一页摘要

relay-rs 使用 Noise,把公私钥材料转化成短期加密会话。ECDH 是数学构件;Noise 是握手框架;WireGuard 是建立在 Noise 设计上的生产级 VPN 协议;Tailscale 则在 WireGuard 之上增加登录、Peer 发现、ACL、NAT 穿透和密钥分发等协调控制面。

当前实现必须区分两条路径:UDP direct 的 Noise 会话位于两个边缘节点之间,C 不持有该会话的 transport key;secure TCP fallback 则是两条 edge↔C 安全链路,C 会终止连接、解密并路由明文 Ethernet frame。前者可以讨论对 C 的端到端机密性,后者不能。

关键词汇:

  • id_x25519:当前 Noise responder 的长期 static DH key,不是 Ed25519 签名密钥;
  • Ed25519:可用于证明设备持有自己的长期签名私钥;当前 relay-rs 未使用;
  • session key:短期数据面加密密钥;
  • auth token:控制面 bearer credential;
  • pair secret:限制到一对节点的授权材料;
  • public key pinning:确认 Peer key 是否符合预期。

重要边界:

中心签发授权,不等于中心签发加密密钥。
中心不签发密钥,也不等于所有数据路径都对中心保密。

在 UDP direct 上,中心不直接签发或持有数据面 session key 是好事,但仅凭这一点还不能防止 MITM。如果中心可以静默替换 Peer 公钥,它仍可以让 endpoint 与中心而不是预期 Peer 派生密钥。要减少这种信任,需要 pin 公钥、签名 peer map、TOFU 告警,或其他不受中心单方面控制的公钥验证方式。secure TCP fallback 则明确由 C 终止两段安全通道,C 本来就在数据机密性的信任边界内。

Forward secrecy 在使用并清除新鲜 ephemeral key 的前提下,可以在长期密钥日后泄露时保护过去会话。Rekey 替换 session / transport key;key rotation 替换长期 identity / control-plane key;node revocation 撤销节点参与权;compromised-key response 是密钥泄露后的运行手册。

对实验项目而言,只要诚实记录边界,当前形态可以接受:Noise 派生 transport key,pair-scoped authorization,没有完整生命周期。内部工具还需要节点注册表、key pinning 或 signed node map、带 TTL 的 pair secret、会话寿命、吊销、日志和泄露响应检查表。开放产品还需要威胁模型、签名控制面状态、可靠的吊销与轮换体验、安全存储、fuzzing 和外部审查。

工程判断:

现在不要声称具备产品级安全。
先把信任边界说清楚,再按项目成熟度增加真正需要的生命周期能力。
10

参考资料