Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

构建许可与安全

构建许可(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_kbpsmax_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);永远不要用 *

另见