安全设计依据
本页记录那些代码无法展示其理由的安全规则: 针对每个信任边界, 列出规则、被否决的设计, 以及原因。本页不描述机制, 也不描述残余风险; 两者都由信任边界台账负责。只有当未来的某个变更有可能重新引入被否决的设计时, 才会存在对应的一行。
客户端程序准入与客户端白名单
| 规则 | 被否决的设计 | 原因 |
|---|---|---|
| 准入以经过证明的锚点 (代码签名者或镜像哈希) 为键; 客户端名称只是日志标签 | 按客户端自行声称的名称 (GENKAN_CLIENT_NAME) 准入 | 名称只是任何进程都能放进环境变量的一个字符串; 锚点才是内核和 Security 框架就正在运行的镜像所作的证词 |
已签名的客户端用显式的 pair-client --signer 锚点配对 (--this-parent 总是固定镜像哈希); 哈希锚点用于未签名或 ad-hoc 构建, 以及 Linux | 为每个客户端固定精确的镜像哈希 | 免费的 Apple Development 证书大约每周重新签名一次, 每次都会改变 cdhash, 所以哈希锚点将需要每周重新配对; 每周唠叨一次的控制措施最终会被关掉 |
| 第一个启动服务器的进程永远不会被自动登记 | 首次使用即信任 (trust on first use) | 静默的首次使用授权会把名额交给抢先竞速成功的那个进程; 登记是用户的行为 |
尚未配对任何客户端 (trust.json 中 "clients": null, 或根本没有记录) 意味着不强制准入, 并在每次启动时以 ERROR 级别记录日志; 损坏或不可读的记录则拒绝所有人 | 把损坏的文件读作「未登记」 | 把加载失败读作未登记, 就是失败即开放 |
| 吊销最后一个客户端后留下一个不准入任何人的空列表 | 最后一条记录删除时把列表重置为 null | null 是首次安装的起始状态; 「用户吊销了所有客户端」必须读作已锁定, 绝不能读作已重置 |
主机身份与证明
| 规则 | 被否决的设计 | 原因 |
|---|---|---|
| 在编译进了证明机制的地方, 只有当桥接对端运行的二进制与接受方相同, 才接受该对端; 各操作系统的状态见台账中的表格 | 一个可通过环境变量配置的受信任镜像哈希列表 | 同用户进程能通过环境变量设置的列表, 它就能把冒名者的哈希追加进去; 「与我相同的二进制」在构造上就无法伪造, 也无需任何配置 |
| 自身身份在启动时测量, 早于绑定 (bind)、接受连接 (accept) 或发起连接 (dial) | 在首次使用时才延迟测量自身 | 之后替换二进制文件无法重新定义「自身」, 进而被当作匹配的对端接受 |
吊销与紧急开关
| 规则 | 被否决的设计 | 原因 |
|---|---|---|
| 每个请求都重新读取一次信任记录并据此重新决定准入; 分作用域的纪元标记只是给监视器的变更通知, 只比较是否相等, 从不比较先后 | 只在计数器前进时才重新决定 | 篡改者可以写入任意值, 而一次未能持久化的递增会让计数器停留在旧值, 因此以计数器为门禁的决定可能为已吊销的客户端提供服务; 直接从记录本身作出的决定则不会 |
| 在紧急开关 (kill switch) 已启用的状态下启动的主机, 以控制平面模式保持运行 | 拒绝浏览器附加, 让主机退出 | 扩展每两秒重新拉起一次主机, 所以一次 kill 会变成一个只能靠 CLI 才能走出的崩溃循环; 控制平面模式让状态查询与启用 (engage) 操作仍然可达 |
| 扩展的紧急开关镜像在缺失时放行, 在格式错误时拒绝 | 把缺失的镜像当作已启用紧急开关 | 全新安装从未收到过主机的消息, 而主机无论如何都会强制执行; 本该是记录的位置出现垃圾数据, 则是有人往那里写过东西的证据 |
登记与用户在场
| 规则 | 被否决的设计 | 原因 |
|---|---|---|
授予或恢复能力的 CLI 仪式 (pair、pair-client、unkill、policy set) 在任何提示之前先拒绝非终端的 stdin, 其在场证明是在该终端上键入的一句话 | 先提示, 再检查终端; 或接受来自任何 stdin 的那句话 | 后台脚本绝不能向用户闪现一个无法解释的提示; echo release | genkan unkill 无法重新打开桥接 |
主机密钥是一把软件 P-256 密钥, 存放在操作系统凭据存储中 (keyring: 钥匙串、凭据管理器、Secret Service), 或存放在用户用 pair --file-store 选择的文件中, 在 Unix 上仅所有者可读, 在 Windows 上受运行时目录的 ACL 保护 | 一把只在 macOS 上可用、每次使用都要求该系统生物识别提示的硬件绑定密钥 | 逐次使用的在场来自浏览器的 WebAuthn 认证器, 在每个平台上都如此, 这让主机密钥只剩一项工作, 即向扩展标识这次安装, 而软件密钥在任何地方都能做到; 硬件密钥把在场绑死在 macOS 上, 让 Linux 和 Windows 没有授予通道。同用户可读是接受的收窄 |
--file-store 是显式选择, 文件的存在本身就是这个选择; 凭据存储失败会被报告, 绝不静默改道到文件 | 存储出错时回退到文件 | 锁定或拒绝的存储与不存在的存储无法区分, 静默回退会在用户不知情的情况下把密钥搬出存储 |
| 扩展所请求操作的在场, 是由主机对照它登记的凭据验证的一次 WebAuthn 断言; 只有在请求的规则不容许任何已登记凭据时, 扩展的窗口才可以担保 | 让扩展报告是哪个界面满足了在场 | 主机是执行点, 它未经验证的裁决就是浏览器一侧任何代码都能置位的一个比特; 对已登记的浏览器给出窗口应答, 就是在场阶梯所禁止的降级 |
| 凭据登记在某一个浏览器的标识之下, 只应答该浏览器的请求; 登记另一个浏览器需要来自任一已登记凭据的断言 | 一个凭据池应答所有浏览器 | 操作是为发起请求的那条浏览器连接执行的, 第二个浏览器的认证器替第一个担保, 会让一个拥有自己密钥的配置文件解除针对另一个配置文件启用的开关; 登记时的跨浏览器许可正是让第二个浏览器得以登记的条件 |
| 机器上一个凭据都没有时的首次登记是首次使用即信任, 在写入时于信任记录锁下重新检查; 此后的每次登记都需要来自已登记凭据的断言 | 以 CLI 的终端确认为首次登记把关; 或从不首次使用即信任 | 全新机器上没有任何凭据可以担保, 所以只能二选一: 首次使用, 或终端下限, 而下限会让可脚本化的 pty 成为每个浏览器的信任根; 锁一侧的重新检查关闭了两个浏览器同时读到空存储的竞争 |
| 验证失败的断言被拒绝并审计, 请求被消耗; 凭据保持登记 | 断言失败时注销该凭据 | 断言失败常常是过期的请求或被取消的提示, 而非攻击; 签名计数器已经能抓住计数型认证器的克隆; 失败即注销会让任何能发出一个坏帧的人移除该浏览器唯一的硬件路径, 把它降级到窗口 |
WebAuthn 校验器只接受 attestation: "none", 只接受 ES256 (COSE 算法 -7, P-256), 并在任一方计数时拒绝 signCount 未前进的断言 | 解析各种证明格式并信任认证器的证书链; 接受密钥提供的每一种 COSE 算法 | 主机的信任锚是它在本机登记时记录下的凭据密钥, 所以证明链只增加解析面而不增加信任; 只用一种算法让随附的密码学实现限于依赖图中已有的纯 Rust p256 校验器 (同一个 crate 也编译了签名的那一半, 但 WebAuthn 路径永远不会触及它, 因为主机不持有凭据私钥); 停滞的计数器意味着被克隆的认证器或重放 |
策略签名
| 规则 | 被否决的设计 | 原因 |
|---|---|---|
| 主机对存储的策略字节原样签名, 扩展对收到的字节原样验证 | 签名前先把 JSON 规范化 | 规范化是解析器差异 (parser differential) 的制造厂; 没有任何东西需要归一化, 归一化器里就没有任何东西可被利用 |
| 只收紧的策略不需要签名, 以未签名形式传输; 只有授予才需要付出一次签名 | 每次策略写入都要求签名 | 为例行收紧弹出在场提示, 会让用户觉得提示是例行公事, 这正是点按钓鱼所需要的条件反射; 伪造的限制只能移除能力 |
MCP 服务器行为
| 规则 | 被否决的设计 | 原因 |
|---|---|---|
已知并保留的缺口: 参数格式错误的 tools/call (没有 name, 或 arguments 既不是对象也不是 null) 会在到达我们的处理器之前被 rmcp 以 -32601 拒绝, 因此不会触发紧急开关检查, 也不会产生审计记录 | 我们自己预先解析每一帧以便审计 | 这条路径上不可能发生任何浏览器操作, 而在 SDK 旁边再放一个解析器只会增加审计面; 审计日志只记录格式正确的调用 |
Native Messaging 协议
| 规则 | 被否决的设计 | 原因 |
|---|---|---|
run.lock 以宽松方式解析; 若某项变更不允许旧读取方继续存活, 则换用新的、带版本号的文件名 | 像其他每一份磁盘记录一样拒绝锁文件中的未知字段 | 锁文件是唯一一个由较旧构建读取、同时由较新的中介 (broker) 写入的文件, 而且它是发现机制, 不是授权; 换用新文件名后, 旧二进制看不到锁文件, 于是失败即关闭 |