# 代理批准的 Touch ID 回退路径安全吗？

Touch ID 批准流程会以各种普通方式失败：笔记本电脑合上了盖子，外接键盘不符合要求，传感器没有识别手指，或者 macOS 在多次失败后锁定了生物识别。如果代理能把这些情况变成含糊不清的批准状态，系统就已经犯下了危险的错误。回退路径必须明确说明操作仍在等待、已经确定被拒绝，还是在等待一次独立的认证事件。

这听起来像界面问题，直到代理手里拿着 SSH 权限，或正在准备经过认证的 API 请求。此时每一个含糊状态都会变成实际的运行行为。除非网关返回精确结果，否则代理无法理解“稍后重试”。当屏幕上只有一个迟迟不消失的生物识别提示，而请求是否仍然有效也不清楚时，人类同样很难做出正确判断。

我采用的设计原则很简单：生物识别失败可以延迟请求，但绝不能扩大权限。传感器不可用可以让流程转交人工决定，但绝不能让代理自行选择更弱的路径。锁定的凭据保险库会暂停所有依赖密钥的工作，直到规定的门槛再次打开。

## Touch ID 可用不等于操作已获批准

Touch ID 回答的是：macOS 此刻能否通过生物识别传感器验证某个人。批准回答的是：这个经过验证的人是否授权了当前代理进程或拟执行的操作。把两者当成同一个事件，会产生糟糕的回退行为，因为失败原因属于不同层次。

按会话批准可以是一个简单的人工决定：用户查看发起请求的进程身份，然后接受或拒绝这次运行。按调用批准则会再次询问，因为凭据所有者认为某个密钥足够敏感，需要逐次确认。这两种决定都不能说明加密凭据保险库当前可用。保险库可能仍处于锁定状态，Mac 可能停留在登录窗口，物理传感器也可能无法访问。

Sallyport 让这种分离真正发挥作用，而不是停留在概念上。它的保险库门是绝对的：保险库锁定期间，所有操作都会被拒绝。只有保险库门允许操作后，按会话授权和按调用密钥才会决定代理权限。这样，界面友好的批准卡片就无法绕过锁定的保险库，变成后门。

这种区分也能避免一个常见但粗糙的建议：“如果 Touch ID 失败，就显示一个普通的批准按钮。”对于本来就允许单击的人工授权提示，这可能是正确的。如果失败的 Touch ID 请求本身负责保护凭据保险库的访问，这样做就是错误的。同一个界面可以同时包含这两个概念，但它们必须产生不同的状态变化。

代理协议也应使用独立状态：

- `awaiting_human_approval` 表示操作尚未运行，人工可以批准或拒绝已明确标识的请求。
- `awaiting_vault_unlock` 表示操作无法继续，因为密钥边界处于关闭状态。
- `denied` 表示网关不会为此请求保留权限。
- `expired` 表示请求等待时间过长，必须重新提交。

不要把这四种状态都叫作“需要批准”。这句话掩盖了代理最需要知道的事实：它应该等待、停止，还是准备一个新的请求。

## 合盖模式会移除内置传感器

MacBook 合上屏幕后，内置 Touch ID 传感器在物理上无法访问。Apple 将合盖模式列为 macOS 中内置生物识别传感器不可访问的典型情况：合上的 MacBook 连接外接显示器和键盘后，无法使用内部 Touch ID，除非外接键盘本身带有 Touch ID。

对于运行编程代理的人来说，这并不是什么边缘情况。MacBook 之所以放在桌下或显示器旁边，正是因为它需要长时间保持运行。如果批准设计假定用户随时能摸到电源按钮，它在演示时可能正常，在日常使用中却会失败。

在决定回退行为之前，先记录实际的桌面布局：

| 物理布局 | Touch ID 路径 | 正确的网关行为 |
| --- | --- | --- |
| 笔记本电脑打开 | 可能可以访问内置传感器 | 在控制策略允许时提供 Touch ID。 |
| 笔记本电脑合上，外接键盘不带 Touch ID | 内置传感器不可用 | 不要提示用户扫描。显示控制策略实际允许的批准路径，或者在保险库仍锁定时拒绝依赖密钥的操作。 |
| 笔记本电脑合上，外接键盘带 Touch ID | 外部传感器可能提供 Touch ID | 只有在 macOS 报告生物识别可用后，才提供扫描。 |
| 笔记本电脑合上，Touch ID 键盘断开或没电 | 没有可用的生物识别路径 | 将生物识别批准标记为不可用，并遵循传感器不可用时的同一规则。 |

表格中最重要的词是“可能”。硬件存在，并不能证明它当前可用。无线键盘可能关机、断开连接、配对到了另一台电脑，或者根本不属于当前用户会话。应用应先向操作系统确认所需策略是否能够运行，然后再显示提示用户触摸传感器的文字。

不要因为进入合盖模式，就自动把操作从按调用批准降级为按会话批准。这种变化会超出眼前的物理问题，并授予比用户看到的更宽权限。如果请求需要按调用决定，就保持按调用。若控制策略允许，提供单击批准；否则根据操作类别让请求等待或失败。

“用户无法扫描”和“用户无法批准”是两回事。使用普通外接键盘的人仍然可以阅读卡片并单击批准按钮。对于设计为单击或 Touch ID 批准的按调用密钥，这已经足够。但它不能解锁由 Touch ID 保护的保险库。界面文字也要同样直接：“批准可用，保险库已锁定”远好于一个笼统的红色失败提示。

## 扫描失败应让一个请求等待，而不是创造新权限

指纹扫描失败一次很正常。皮肤干燥、手指角度不佳、传感器上有残留物，以及匆忙触碰都可能发生。正确的回应是有边界的重试状态，既不是立即拒绝，也不是让请求无限期保持有效。

Apple 的 LocalAuthentication 文档区分了普通认证失败和生物识别锁定。凭据检查失败会报告 `authenticationFailed`，而尝试次数过多后会进入另一个独立的锁定状态。网关应保留这种区分，因为两者的恢复路径不同。

对于普通扫描失败，让原始操作在短时间内保持不可变并处于等待状态。用户应看到正在批准的内容、发起请求的代理进程、目标主机或 API、凭据标签，以及有实际意义的操作。代理则应收到机器可读的等待结果，而不是一个伪装成失败的超时。

可以使用这样的响应结构：

```json
{
  "status": "awaiting_human_approval",
  "request_id": "apr_7f3c",
  "reason": "biometric_retry",
  "expires_at": "2026-07-22T18:42:00Z",
  "retry_after_ms": 1500,
  "action_started": false
}
```

`request_id` 必须绑定完整的拟执行操作，而不只是凭据。如果代理先请求运行 `git push`，之后在用户重试 Touch ID 时更改远程仓库、分支或命令，那么这就是另一个请求。应拒绝它，或要求创建新的批准卡。因为同一个进程仍然存在就复用批准，正是把无害的重试变成混淆代理漏洞的方式。

设置两个限制。第一，要对界面尝试进行足够的限速，避免有故障的代理不断重新打开会吸引注意力的提示。第二，在短暂且清晰可见的时间后让等待中的请求过期。用户十分钟后回来时，应针对重新渲染的请求做决定，因为代理的仓库状态、API 负载或远程系统可能已经改变。

重试状态不应让代理进程持有任何凭据材料。代理可以保留预定执行的命令或请求正文，但网关不能在“等待认证”期间把令牌交出去。只有网关执行已批准的通道操作时，才注入密钥。

团队经常把错误的重试循环放在错误的位置。他们让代理每隔几秒重试整个工具调用。这样会产生重复卡片，增加 API 操作重复执行的机会，也会教会模型把坚持不懈当成绕过犹豫的方法。等待中的请求由网关负责管理。代理可以轮询或等待这个请求 ID，但在首个请求过期或被拒绝之前，不应自行制造新请求。

## 传感器锁定意味着生物识别路径已经结束

传感器锁定并不是“再按一次指纹”就能解决的请求，而是 macOS 在告知用户：生物识别已被禁用，必须完成操作系统要求的恢复步骤。Apple 将 `biometryLockout` 记录为多次失败后进入的状态，并说明需要密码才能解锁生物识别。

这对代理操作有直接影响：停止提示 Touch ID。操作系统已经锁定传感器后，继续要求用户扫描指纹既具有误导性，也可能让用户反复用力触碰传感器。应告诉用户 macOS 需要账户认证来恢复 Touch ID，然后根据操作类别结束或暂停网关请求。

不要把账户密码悄悄当成 Touch ID 的等价物。Apple 的 `deviceOwnerAuthentication` 策略可以使用 Touch ID、附近已配对的 Apple Watch，或用户的 macOS 密码。而仅生物识别策略在生物识别不可用、未注册或已锁定时会失败。两者都是有效的系统策略，但作出的安全承诺不同。

如果保险库门规定 Touch ID 是解锁条件，那么密码回退就改变了这道门。你可以在另一种产品设计中决定 macOS 密码是可接受的替代认证方式，但必须在保险库边界处明确声明并实现这一选择。不要因为框架提供了方便的默认值，就意外继承这种行为。

对于绑定 Touch ID 的保险库，锁定应为当前所有等待中的依赖密钥调用返回终止性结果：

```text
status: denied
reason: vault_authentication_unavailable
recovery: authenticate with macOS to restore Touch ID, then submit a new request
action_started: false
```

这比临时的扫描失败更严格。用户必须在网关批准流程之外完成恢复操作。在恢复期间继续保留旧请求，会产生难看的歧义：之后输入账户密码，是批准了原来的 SSH 命令，还是只是恢复了传感器？答案必须明确：它只恢复了传感器。代理必须重新请求该操作。

对于不依赖保险库解锁的批准，可以选择不同结果。如果策略允许，按会话授权卡可以继续作为单击决定使用，前提是操作本身不需要被锁定的密钥。界面应明确指出失败的具体对象。“Touch ID 已锁定，仍可单击批准”是清晰一致的消息；“认证失败”则不是。

## 让等待还是停止成为操作自身的属性

不要只根据错误代码决定是否等待。应结合操作的后果、时效要求以及凭据边界的状态来决定。同一个不可用的传感器，可能让无害的状态查询等待，让不可逆的基础设施命令停止，也可能让依赖保险库的请求在保险库解锁前直接失败。

我使用三类操作。

### 第一类：短暂等待人工响应

只有同时满足以下条件时，才让操作等待：

- 网关尚未开始操作，也没有注入凭据。
- 请求具有稳定身份，并显示明确的过期时间。
- 批准后重新执行不会让用户感到意外，因为目标和负载保持不变。
- 操作可逆、只读，或具有足够幂等性，短暂延迟不会改变其含义。

例如，读取私有软件包版本、获取仓库受保护分支的设置，或发起一个明确标注的模拟 API 请求。即便如此，等待也必须由网关管理，而不是放进代理的重试循环。

### 第二类：停止并要求新请求

如果延迟会改变命令的实际含义，就应停止。部署、生产数据库迁移、强制推送、凭据轮换、支付扣款，或删除数据的 SSH 命令，都不应带着潜在批准一直等待。用户稍后再次看到它时，应获得一张反映当前环境的新卡片。

如果请求包含临时值，也应停止。签名 API 请求、一次性部署产物、短时有效的 URL，或本地工作区已经改变的命令，都不应从过时意图中恢复。代理进程尚未退出，并不能证明它仍然想做完全相同的事情。

### 第三类：保险库关闭时立即拒绝

锁定的保险库优先于便利。如果拟执行的 HTTP 调用或 SSH 命令需要存储的密钥，而保险库门处于锁定状态，就应拒绝操作，不要把它排队等待解锁后自动执行。用户可以先解锁保险库，然后让代理提交新请求。这样可以保留清晰的因果记录：先解锁，再提交，最后执行。

这正是 Sallyport 固定决策阶梯的价值所在。保险库锁定时，它的保险库门会拒绝所有操作；解锁后，再由按会话授权和按调用批准处理请求。策略不会试图判断延迟中的 `curl` 是否足够无害，可以在生物识别事件发生后重新唤醒。

一个诱人的替代方案是执行队列，等用户触碰传感器后自动唤醒。它很受欢迎，因为演示效果流畅。但在真实使用中，它会把认证变成触发一批可能已经不再符合用户意图的工作的开关。批准应该释放一个用户仍然看得见的请求，而不是清空用户离开期间积累的队列。

## 外接 Touch ID 键盘需要可用性检查

外接 Touch ID 键盘解决了合盖模式下的一个问题，却增加了另一个依赖。批准发生时，键盘必须存在，并且在当前 Mac 会话中可用。应把这视为实时条件，而不是一次性设置事实。

Apple 的 LocalAuthentication 错误包括可拆卸生物识别配件的 `biometryDisconnected` 和 `biometryNotPaired`。这些代码很重要，因为它们能区分硬件缺失和用户认证失败。断开的键盘不应消耗重试次数，也不应计入用户失败率。

界面和协议应分别处理以下四种状态：

| 状态 | 用户看到的内容 | 代理收到的内容 |
| --- | --- | --- |
| 传感器就绪 | 清晰的请求和 Touch ID 操作提示 | `awaiting_human_approval` |
| 合盖模式下传感器无法访问 | 说明内置传感器无法触达 | `approval_path_unavailable`，或允许的单击路径 |
| 外部传感器断开 | 提示重新连接、充电，或使用允许的替代批准方式 | `approval_path_unavailable` |
| 扫描被拒绝 | 同一个不可变请求和重试提示 | 带有 `biometric_retry` 的 `awaiting_human_approval` |

不要把框架原始错误名称直接展示给用户，但要在本地活动记录中保留它们。“外接 Touch ID 键盘不可用”对用户有帮助，`LAError.biometryDisconnected` 则应留在诊断信息和测试中。

批准卡片也必须能够在没有传感器时操作。这既是安全问题，也是基本的可用性要求。鼠标、触控板、键盘焦点和辅助功能控件仍应允许用户拒绝请求，或选择获准的单击批准。不可访问的生物识别传感器绝不能把用户困在无法回答的提示中。

## 批准界面必须说明被阻塞的边界

大多数困惑来自一个试图表达所有认证形式的通用弹窗。应根据正在等待的边界拆分消息。

对于按会话请求，先显示发起请求的进程的代码签名权限，然后让用户清楚地选择批准或拒绝。这一刻决定了该代理运行是否可以执行操作。如果 Touch ID 可用，它可以确认这个选择。如果设计允许单击，合盖模式不应把卡片变成死路。

对于按调用密钥，显示准确的操作和凭据标签。只有当单击批准只适用于一个不可变请求并且很快过期时，它才仍然是按调用决定。不要因为桌面上使用 Touch ID 不方便，就让代理把五次调用绑定在一个按钮后面。

对于锁定的保险库，应说明保险库已锁定且操作尚未开始。不要把消息写成代理请求被拒绝，因为用户可能会以为自己需要再次单击。恢复步骤属于保险库规定的认证机制。保险库打开后，任何需要密钥的操作都必须重新提交请求。

良好的审计记录也应区分这些状态变化。例如：

```text
2026-07-22T18:40:12Z request.created     id=apr_7f3c channel=ssh action="git push origin main"
2026-07-22T18:40:13Z approval.pending   id=apr_7f3c method=touch_id
2026-07-22T18:40:16Z biometric.failed   id=apr_7f3c source=built_in_sensor
2026-07-22T18:41:02Z request.expired    id=apr_7f3c action_started=false
```

操作日志不应假装扫描失败就是授权拒绝。那是一次认证尝试失败。请求在未执行的情况下过期了。这些措辞在审查时很重要，尤其是代理说“无法部署”，而操作人员需要知道究竟是系统阻止了它、用户拒绝了它，还是根本没人完成提示。

对于防篡改审计系统，应在把状态变化返回代理之前先记录它。Sallyport 从一个加密、哈希链式的日志中生成 Sessions 和 Activity 日志，其 `sp audit verify` 命令可以在离线状态下对密文验证哈希链。这样，后续审查人员无需相信代理自己的记录，也能确认请求是过期还是被拒绝。

## 测试实际桌面，而不只是顺利的 API 路径

能够返回成功和失败代码的 LocalAuthentication 单元测试是必要的，但几乎无法证明批准流程真的可靠。让用户感到不便的失败，往往出现在硬件布局、桌面状态和代理时机交汇的地方。

在宣布流程完成前，先在真实 Mac 上执行以下测试：

1. 在笔记本电脑打开时启动代理会话，提交一个按调用请求，拒绝它，然后提交新的请求并批准。确认被拒绝的请求从未执行。
2. 合上屏幕盖，连接外接显示器和不带 Touch ID 的键盘。确认界面不会要求用户触摸无法访问的传感器。分别测试允许单击的批准和依赖保险库的请求。
3. 使用正常工作的外接 Touch ID 键盘重复测试。在批准等待期间断开键盘或关闭电源。确认请求报告的是硬件不可用，而不是生物识别失败。
4. 连续进行失败扫描，直到 macOS 进入锁定状态。确认生物识别提示停止，依赖密钥的请求不会排队等待以后执行，并且恢复后必须重新提交操作。
5. 让等待中的请求自然过期。在发送新请求前更改仓库分支、命令参数或 API 负载。确认新的提案获得不同的请求 ID 和新的人工决定。

每次测试后都检查日志。你应看到一次请求创建事件、一系列状态变化，以及一次执行事件或完全没有执行事件。一次批准信号对应多次执行，说明存在重放或重试缺陷。缺少终止事件则意味着支持人员只能猜测代理是否仍在等待。

还要测试取消。Touch ID 不可用时，用户必须仍能拒绝请求；代理必须能够放弃等待中的请求；应用关闭时必须使仍然有效的批准失效。Apple 在 LocalAuthentication 中区分用户取消、应用取消和系统取消。即使界面把它们都归为易懂的“已取消”，审计模型也应保留这种区分。

可靠的回退路径正常工作时几乎让人觉得无聊。界面如实说明传感器状态，代理收到可以遵循的状态，密钥留在保险库中，没有任何操作会因为用户合上笔记本电脑而逃逸。这就是应坚持的标准：每一次物理故障都导向一个明确且不会扩大权限的结果。
