日期: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、节点吊销、长期密钥轮换、密钥泄露响应。
本文面向后端工程师,不是密码学证明。目标是把工程边界说清楚。
心智模型
系统分为两个平面,但数据面的信任边界还要继续按实际路径拆开:
控制面:
身份、授权、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 数据机密性的信任边界内。
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 无条件信任控制面提供的公钥,控制面仍然处在身份信任边界内。
密钥、身份与 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 现在仍被授权吗?
这个节点健康吗?
这条路由应该被允许吗?
后面这些属于策略问题。
生命周期术语
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 获取新的信任状态?
没有这份运行手册,吊销和轮换都只能算局部功能。
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,
没有产品级节点吊销,也没有自动化的密钥泄露响应。
容易混淆的地方
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 确实是预期节点的唯一证明。
成熟度层级
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;
- 在安全主张进入宣传材料前完成独立审查。
relay-rs 实际建议
近期不要试图变成 WireGuard 或 Tailscale。更有价值的下一步,是把当前信任边界表达清楚。
建议任务:
-
按数据路径准确记录中心 C 的职责:
UDP direct:协调连接,不持有 edge↔edge transport key,不处理明文帧 secure TCP fallback:终止两侧 Noise 通道,处理并路由明文帧 -
增加 Peer 身份表:
peer_id -> expected id_x25519_public -> status -
把 pair secret 绑定到:
initiator_id, responder_id, initiator_pub, responder_pub, issued_at, expires_at, nonce -
决定 pair secret 是:
加密 payload 内的授权材料还是:
作为 PSK 混入 Noise不要把两者描述成同一回事。
-
即使通过重连实现,也要设置最大会话寿命:
运行 N 分钟或传输 M 字节后关闭并重新握手 -
增加人工吊销:
disabled 节点不能建立新会话 观察到吊销后关闭现有会话 -
编写
SECURITY.md,明确当前 non-goal。
当前适合声明的 non-goal:
本项目尚未提供产品级密钥轮换、自动节点吊销或密钥泄露恢复。
中心签发的 pair secret 用于授权节点对,不直接作为数据面加密密钥。
UDP direct 对 C 隐藏数据面明文;secure TCP fallback 不提供对 C 的机密性。
当前信任模型假设 Peer 公钥来自可信来源或已经被 pin。
一页摘要
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 和外部审查。
工程判断:
现在不要声称具备产品级安全。
先把信任边界说清楚,再按项目成熟度增加真正需要的生命周期能力。
参考资料
- Noise Protocol Framework:noiseprotocol.org/noise.html
- TLS 1.3 RFC 8446:rfc-editor.org/rfc/rfc8446
- X25519 RFC 7748:rfc-editor.org/rfc/rfc7748
- Ed25519 RFC 8032:rfc-editor.org/rfc/rfc8032
- WireGuard Protocol & Cryptography:wireguard.com/protocol
- Tailscale control and data planes:tailscale.com/docs/concepts/control-data-planes
- Tailscale how it works:tailscale.com/blog/how-tailscale-works