# 代理二进制文件更新应在什么时候失去信任？

本地代理的批准应当只适用于你检查过的那个可执行文件、你批准的那个进程，以及它当时请求的访问权限。一旦可执行文件发生变化，旧批准在实际意义上就已经失效，即使文件名、应用包标识符和图标都没有改变。

只有当你调查过一个原本批准给 1.8 版本、后来却被 1.9 版本悄悄使用的权限时，这条规则才会显得严格。许多错误决定都始于过于宽泛的身份检查：路径、产品名称，或熟悉的签名 Team ID。这些检查可以作为证据的一部分，但任何一项都不应单独跨越代码变更继续承载特权信任。

## 信任属于被观察到的可执行文件

本地代理不是发布者身份，而是一个具体的签名程序。它从特定路径加载，由特定父进程启动，带有特定的参数和环境，并请求执行一组特定操作。一个发布者可以发布许多程序，同一个程序在不同版本之间也可能发生重大变化。稳定的名称几乎无法说明这两件事。

当代理能够调用付费 API、使用 SSH 凭据、修改代码仓库或向计算机外发送数据时，这种区别尤其重要。批准不是对供应商的赞许，而是允许某个进程执行会产生后果的操作。

请将以下身份分开记录：

- 文件身份是磁盘上经过签名的代码及其摘要。
- 签名身份是 macOS 为该代码报告的授权者。
- 发布者来源是证明文件通过预期发布路径送达的证据。
- 运行时身份包括启动它的进程，以及它请求使用的访问权限。

团队经常把这四者合并成一句「这是我们的代理」。于是，位于同一路径的替换文件就继承了它从未获得过的访问权限。安全规则很简单：替换可执行文件后，必须重新做出信任决定。

哈希可以检测替换，但不能说明替换文件由谁签署。签名可以识别签署者，但不能证明下载渠道。干净的下载也不能告诉你新版本是否需要更广泛的访问权限。三项检查都需要，因为每项检查发现的是不同类型的故障。

也不要走向另一个极端，永远固定每一个字节。构建版本会正常变化，证书会轮换，发布版本也需要更新。重点不是永久不变，而是有人看到变化，确认发生了什么，然后明确决定原先请求的权限是否仍然合理。

## 代码签名回答的问题，比多数人以为的更窄

macOS 代码签名让操作系统验证签名代码自签署者生成签名后是否被修改。对于 Developer ID 分发，Gatekeeper 还会评估开发者身份以及它对项目的判断。这些都是有用的证据，但不是完整的供应链结论。

Apple 的技术说明 TN2206《macOS Code Signing In Depth》将代码身份与系统接受代码时所依据的更广泛条件区分开来。其中对 designated requirements 的讨论与这里尤其相关：macOS 可以使用一条要求，把未来版本识别为属于同一代码身份。这种连续性有助于普通应用更新，但对于能够花钱或使用私有基础设施的代理来说，仅凭这一点太弱。

有效签名无法回答以下问题：

- 发布者是否确实希望这个确切版本到达你的计算机？
- 签署它的发布者凭据是否已经遭到入侵？
- 新版本是否增加了会改变风险的能力？
- 安装程序、更新器或启动脚本是否替换了某个组件，而你检查的部分却没有变化？

Gatekeeper 和公证降低了 macOS 运行明显不受信任软件的概率，但不会因为代理签名有效，就把它变成获准使用你运营权限的主体。请把操作系统允许其运行和批准其执行特权操作视为两个独立决定。

证书续期也是如此。续期可能只是正常的管理事件，但它仍然改变了你所依赖的证据。如果 Team ID、标识符和预期的发布者路径都保持一致，操作人员可以在审核后批准更新。如果签署权限转移到了另一家组织，应停下来向发布者寻找清晰解释。不要因为应用可以无警告启动，就接受出人意料的身份变化。

## 发布者来源与签名是两回事

来源关注二进制文件如何到达你手中，以及这条路径是否符合发布者通常的发布方式。一个从未知聊天附件复制而来的文件即使带有签名，也只能告诉你是谁签署了这份副本，不能解释你为什么会在那里收到它。

对于生产环境中的代理，安装或更新时应记录一份简短的发布收据。它可以是管理该代理的代码仓库中的文本文件、变更记录，或内部发布日志中的一条记录。收据应包含版本、安装来源、观察到的签署权限、Team ID、摘要、审核日期，以及接受该版本的人员。如果发布者提供了发布摘要，也一并记录。

发布渠道需要人工做一次合理性检查。更新器是否来自预期的应用？发布者是否为该版本发布了版本说明？归档文件名、软件包签名和目标路径是否符合通常的安装方式？通过新渠道送达的意外更新，应像意外出现的签署者一样受到怀疑。

不要把公开源代码仓库和发布构件混为一谈。仓库可以展示源代码历史，但发布流水线可能构建出不同的二进制文件。反过来，即使你无法复现构建过程，签名的二进制文件也可能是合法的。这是不同的保障等级。请说明你拥有哪一级证据，不要假装其中一项能够证明另一项。

当发布者提供了经过认证的校验和时，比对摘要很有帮助。如果你从同一个不受信任的页面复制校验和和二进制文件，比对就没有意义。有效的比较对象应来自发布者独立控制的发布记录、可信的软件包管理器记录，或之前建立的内部来源。

## 更新时必须重新审核请求的访问权限

二进制文件更新后，即使签署权限不变，也可能不应继续拥有旧版本的全部访问权限。需要审核的原因不只是担心恶意代码。新的行为可能让旧权限变得不再合理。

请确认更新后进程会执行哪些外部操作。过去只能读取问题元数据的代理，现在可能会创建拉取请求。过去使用一次性测试令牌的代理，现在可能会针对共享主机执行 SSH 命令。新插件、配置格式变化或默认命令变化，都可能改变进程的实际影响范围，而不必向操作系统申请新的权限。

审核请求的访问权限时，应覆盖实际的操作边界：

- 进程会使用哪些 HTTP 主机、账户范围和凭据记录？
- 它可以访问哪些 SSH 目标，并执行哪些远程命令？
- 它会在什么工作目录中运行？哪些仓库钩子、参数和环境变量会启动它？
- 它现在是否会接收来自不同来源的输入，例如拉取请求评论或构建日志？
- 它能否调用之前审核范围之外的其他本地可执行文件？

宽泛的长期批准会在这里失效。「允许这个代理」掩盖了真正重要的部分：允许它做什么，使用谁的凭据，以及响应什么输入？

对于代理进程，输入来源同样值得关注。如果更新后的编程代理会根据不受信任的问题文本采取行动，那么普通的代码仓库访问就可能变成提示注入的入口。代码签名不会检查进程收到的指令，它只覆盖解释这些指令的程序。

请让批准界面和内部记录保持具体。写明进程权限、目标、凭据类别，以及该操作是否会改变远程状态。只显示「代理请求访问」的警报，无法帮助操作人员做出可靠决定。

## 在新代码可以行动前结束旧会话

最清晰的规则是把批准绑定到一个正在运行的进程，并在该进程退出时结束批准。替换二进制文件会产生新的进程，因此也应获得新的批准。这样就不必费力判断某个软件包更新是否足够小，可以继承先前的授权。

不要允许进程原地更新自己，然后继续使用替换前获得的授权。有些更新器正是这样工作：旧进程下载归档文件，将新的可执行文件写到旧路径上，然后启动辅助进程或重新执行自身。如果授权层只检查路径名或长期存在的客户端记录，新代码就会继续沿用旧决定。

实际的决定记录可以如下：

```text
process path: /Users/dev/tools/agent/bin/agent
observed digest: 8a4b...e19c
identifier: dev.example.agent
TeamIdentifier: A1B2C3D4E5
parent: interactive shell in approved repository
requested actions: issue API read, test-host SSH command
approval scope: this process only
expires: process exit
```

不要把摘要当成永久允许列表条目。记录它，是为了确认下一次批准针对的是不同的代码。更新后进程重新启动时，将这次新观察到的结果与上次比较，然后向操作人员展示真正重要的差异。

即使是同一版本的重启，在启动上下文发生变化时也可能需要重新审核。在已检查的代码仓库中由交互式 shell 启动的二进制文件，并不等同于由构建任务在更大环境中无人值守启动的同一文件。可执行文件只是主体的一部分，进程上下文才让它完整。

## 批准替换文件前先检查 macOS 证据

在允许候选可执行文件执行特权调用前，可以使用 macOS 内置工具进行检查。命令应针对即将启动的可执行文件，而不是 Downloads 中名称相似的副本，也不是你以为包含它的外层应用包。

```sh
codesign -d -vvv /Users/dev/tools/agent/bin/agent 2>&1
spctl -a -t exec -vv /Users/dev/tools/agent/bin/agent
shasum -a 256 /Users/dev/tools/agent/bin/agent
```

典型的 `codesign` 输出会包含类似以下字段：

```text
Identifier=dev.example.agent
Authority=Developer ID Application: Example Publisher (A1B2C3D4E5)
TeamIdentifier=A1B2C3D4E5
CDHash=8a4b9c0d...
```

`spctl` 会报告评估结果，并通常会报告已接受的 Developer ID 软件的来源。`shasum` 会打印完整的 SHA-256 摘要，后面跟着路径名。将相关输出与发布收据一起保存。具体措辞会因 macOS 版本而变化，因此应比较身份字段和评估结果，而不是空格或字段顺序。

对于应用包，还要检查其中的嵌套代码。签名的外层应用包可能包含辅助程序、框架或命令行工具。如果代理会直接启动某个辅助程序，就应直接检查该辅助程序。请求访问权限的文件，才是必须与决定绑定的文件。

`codesign` 验证成功，并不意味着可执行文件已经公证，也不意味着它适合你的用途。这只表示按照该命令的验证规则，签名检查通过。事件记录中应保留这种区别，否则之后有人看到「签名有效」，就可能把它理解成「版本已审核并批准」，而后者的含义要大得多。

## 稳定路径很容易让你批准错误的代码

假设本地包装器位于 `/Users/dev/bin/agent`。开发人员因为它只调用只读项目 API，批准了一次。之后包装器自动更新，下载新的辅助程序，并保留同一个路径。批准组件识别到这个路径，于是把旧授权交给了新的辅助程序。

这个过程不需要恶意攻击者参与，发布版本也可能完全真实。问题在于，批准记录写的是「路径等于允许的路径」，而实际决定针对的是一个行为范围更窄的旧程序。

当包装器本身是脚本时，问题会更严重。Shell 脚本经常调用稳定符号链接下的带版本二进制文件，从可写目录读取配置，或从 `PATH` 中选择辅助程序。如果包装器在检查之后仍能重定向执行，那么只检查包装器的签名几乎无法说明最终执行的是什么。

请调整操作顺序。先解析最终可执行文件，再检查其签名和摘要，记录父进程和参数，最后授权这个正在运行的进程。如果启动器在批准后仍能替换或选择另一个可执行文件，就应将操作网关绑定到连接时观察到的进程，并拒绝不匹配的进程。

另一种常见做法是，允许 Team ID 相同的任何更新自动继承权限。团队选择它，是因为提示会打扰开发人员，而发布证书通常保持稳定。对于特权代理，这种做法是错误的，因为它把发布者凭据当成了未来所有行为的空白支票。应通过会话范围的批准、狭窄的访问权限和清晰的更新通知减少提示疲劳，而不是让更新变得不可见。

## 发布者变更需要明确的迁移记录

签署权限发生变化可能是合法的。公司可能收购产品、转移到不同的 Developer ID 账户，或替换旧的分发流程。应把这些事件视为迁移，而不是普通更新。

要求记录一份明确的说明，写明原身份、新身份、发生变化的版本，以及接受变更时使用的证据。良好证据包括发布者既有发布渠道中的签名公告、预期仓库中一致的版本说明，以及通过正常交付路径取得的软件包。一个无法解释的弹窗不是证据。

Team ID 发生变化时，默认应拒绝，直到有人审核迁移。相同 Team ID 下证书发生变化，可以在完成通常的来源和访问审核后重新批准。相同证书下摘要发生变化，同样需要重新批准，因为这是新代码，即使其他字段都一致。

不要授予「接受这个应用名称未来所有身份」之类的宽泛例外。这种例外会把一次性迁移变成永久漏洞。只有在审核者接受了某个具体版本后，才保存新的身份。下一次变化仍应触发相同审核。

如果团队分发内部代理，应在操作人员无需依赖代理本身就能找到的位置发布预期的签名身份、发布摘要和更新流程。当替换文件本身正是审核对象时，代理无法可信地证明自己的替换是安全的。

## 简短的批准协议胜过庞大的例外列表

不需要复杂的规则引擎，也能让更新决定变得可预测。你需要的是一套每次可执行文件变化时都适用的简短协议。

1. 停止旧进程并撤销其活动会话。
2. 解析即将连接或执行操作的最终可执行文件。
3. 将它的摘要、标识符、签署权限和 Team ID 与上一份发布收据比较。
4. 检查预期发布渠道，并记录身份发生变化的原因。
5. 审核请求的目标和凭据，然后批准新进程会话或拒绝它。

这套协议可以将正常维护更新与真正的异常区分开。相同签署权限和正常发布来源下出现新摘要，是需要审核的事件，不是紧急事故。陌生签署权限、意外安装程序以及请求生产环境 SSH 凭据，则应暂停发布，直到有人调查清楚。

Sallyport 的会话授权可以让进程边界变得清晰，因为它会在允许代理运行前识别新的代理进程；逐次调用批准设置则可以让敏感凭据遵循更严格的决定。这不会取消检查更新后代理的必要性，但能防止旧运行悄悄把批准借给后续运行。

记录应保持足够简短，让人们愿意持续维护。真正有用的证据包括观察到的文件、签署者、来源、启动上下文和已批准的操作。如果发布版本改变了其中任何一项，就应让负责风险的人在代理行动前看到变化。

## 审计记录必须区分执行者和发布者

只写着「代理使用了凭据」的审计轨迹，会让事后调查变得不必要地困难。应记录发起请求的进程、macOS 报告的签署权限、授权该操作的会话，以及实际发生的操作。发布者身份说明谁签署了代码，进程记录说明谁执行了操作。

代理运行出问题时，这种区别非常重要。你需要判断是新版本改变了行为，还是旧二进制文件从意外位置运行；是人员批准了与其理解不同的访问请求，还是不受信任的输入引导了一个原本正常的进程。仅有发布者字段无法回答这些问题。

请将完整性和可读性分开处理。防篡改记录可以证明之前的条目是否被修改，却无法在事后补回缺失的事实。应在批准时记录可执行文件摘要和决定上下文，那时你仍然可以观察到它们。

Sallyport 从同一份加密、哈希链式审计日志中生成 Sessions 和 Activity 日志，`sp audit verify` 可以在密文上离线检查这条链。可以用这类证据验证记录，再配合发布收据说明为什么当初向该可执行文件授予访问权限。

我最先会做的运营调整，是删除任何只匹配路径、显示名称或发布者的批准规则。改为批准当前进程，在进程退出时让批准失效，并要求二进制文件替换后重新做出决定。这一条边界就能捕捉到宽泛允许列表经常漏掉的更新路径。
