构建许可与安全
构建许可(Grant) 是把公开的构建服务器绑到私有构建机器的密码学原语。 没有许可,构建机器必须绝对信任构建服务器;有许可,构建机器在本地验证 每个进入的会话,可以拒绝任何与自己嵌入的公钥不匹配的东西。
许可里有什么
#![allow(unused)]
fn main() {
pub struct JobGrant {
pub user_id: UserId,
pub job_id: JobId,
pub issuer_id: IssuerId,
pub issued_at: u64, // unix 时间戳
pub ttl_secs: u32, // 许可 token 是 5..=30
pub execution_params: ExecutionParams,
pub webrtc: WebRtcPermissions,
}
pub struct WebRtcPermissions {
pub allowed_channels: Vec<String>, // 例如 ["espctl", "pty", "firmware"]
pub max_bandwidth_kbps: u32, // 滑动窗口强制
pub max_message_rate: u32, // 每秒消息数
pub ice_servers: Vec<IceServer>,
pub peer_fingerprint: String, // 请求方证书的 SHA-256 指纹
}
}
整个 struct 被编码,然后由构建服务器用加密签名签名。构建机器在编译期 嵌入对应公钥,在执行任何会话之前在本地验证签名。
生命周期
1. 浏览器/MCP 客户端计算自己证书的指纹
(DER 形式证书的 sha-256)。
2. 客户端 POST /grant/request 带上指纹和需要的通道。
3. 构建服务器:
- 鉴权请求方(session/JWT/MCP_AUTH_SECRET)。
- 检查速率限制和配额。
- 挑 ICE 服务(STUN、带轮换凭据的 TURN)。
- 构造一个 JobGrant struct,TTL <= 30s。
- 用签名密钥签名。
4. 客户端收到签名许可 + ICE 服务 + ephemeral job_id。
5. 客户端用 ICE 服务打开自己的 WebRTC peer 连接。
6. 构建机器(在定期查看 /agents/jobs)看到新任务和许可。
7. 构建机器验证:
- 许可签名(对照嵌入的公钥)。
- issued_at + ttl_secs > now。
- peer 证书指纹匹配许可中的 peer_fingerprint
(这发生在连接协商完成后,加密层交出证书时)。
8. 构建机器只执行许可中 allowed_channels 列出的通道。
9. 构建机器按通道执行 max_bandwidth_kbps 和 max_message_rate。
10. 构建结束(或许可过期),构建机器拆除连接。
安全模型假设什么 —— 以及不假设什么
假设:
- 构建机器嵌入的公钥没有被一个已经在构建机器上拿到 root 的攻击者 换掉(如果他真拿到了,游戏已经结束了)。
- 构建服务器的私钥保存在构建服务器主机上没泄露。如果它泄露了,攻击者 可以为任何人铸造许可,但他们仍然不能让构建机器跑超出沙箱允许范围 的任意代码。
- 浏览器的证书指纹对每个会话是唯一的;在每次 peer 连接上重新计算。
不假设:
- 构建服务器被信任来读或修改构建内容。它不行 —— 数据通道在与构建机器 之间端到端加密。
- 客户端和构建机器之间的网络是可信的。WebRTC over TURN 加密整个 payload;中间人只看到 TURN 包裹的密文。
- CORS 或 CSP 单独是足够的浏览器侧保护。指纹绑定让被偷的许可对 任何证书不匹配的人都没用。
通道白名单强制执行
构建机器提供的最具体的安全属性是通道白名单:许可精确列出允许
哪些 WebRTC 数据通道名(例如 ["espctl", "pty", "firmware"]),
构建机器拒绝任何用其他名字打开的通道。
这在构建机器的 on_data_channel handler 中强制执行 —— 在读任何消息
之前,如果 channel label 不在白名单里就关闭它。没有服务端的 opt-out,
也没有按消息的覆盖。
如果未来构建添加新数据通道(例如 metrics 通道),每个需要它的客户端
都需要在 required_channels 中请求新名字。构建服务器会拒绝为不在
运维允许列表中的未知通道名颁发许可。
带宽和速率限制
每个许可携带 max_bandwidth_kbps 和 max_message_rate 数值。
构建机器用滑动窗口强制执行两者:
- 带宽: 拖尾 1 秒窗口的字节计数器。当滚动总和超过上限,写阻塞 (发送变慢),直到窗口前移。
- 消息率: 拖尾 1 秒窗口的消息计数器。同样的强制执行模型。
它们是按通道的,不是按会话的。firmware 通道通常比 espctl 通道
得到大得多的带宽预算。
许可过期 —— 故意短
默认许可有效期是 5-30 秒。这是有意的:
- 不知怎么偷到许可的攻击者只有几秒时间用它。
- 构建服务器密钥泄露的影响范围有限 —— 攻击者能向前铸造许可,但他 不能重放昨天的许可。
- 长时间运行的构建会话(例如交互式串口监控)有单独的、生命周期更长
的“会话许可“,有效期上限由运维配置(通常几分钟)。用户通过许可
请求中的
timeout_secs字段申请更长的会话。
对交互式 PTY 会话,典型模式是周期性地通过重新颁发新许可来刷新,
不丢失底层 peer 连接。客户端和构建机器在 espctl 通道上交接新许可。
用户不需要做任何事 —— 客户端自动处理刷新。
运维 checklist
如果你在运营一个构建服务器:
- 签名密钥保管。 用
aegis-keygen生成密钥,把私钥存进 secrets 管理器(或者最低限度,模式 600 的/etc/aegis/secrets.env),永远不要 check 进仓库。 - 公钥分发。 对应的公钥必须嵌入到你发布的构建机器二进制中。构建
脚本处理这个 —— 你在编译期设
AEGIS_TRUSTED_PUBKEY,构建机器对照 它验证。 - TURN 凭据轮换。 TURN 凭据由构建服务器按会话轮换;你不需要手动 管理。
- CORS pin。 构建服务器只接受
ALLOWED_ORIGINS列出的 origin 发起 的/grant/request。设为你精确的生产 origin(例如https://esphome.cloud);永远不要用*。
另见
- 构建服务器与连接建立 —— 许可怎么颁发。
- WebRTC 构建机器与数据通道 —— 许可怎么强制执行。