# 加密审计日志：离线验证能证明什么

加密审计日志可以在不向外部审查人员透露任何一条记录内容的情况下，提供有用的证据。审查人员可以检查密文记录是否仍按预期顺序排列、较早的记录是否发生变化，以及导出文件从哪里开始与其声明的历史不一致。

这种证据有明确的边界。验证结果干净，并不能证明每条记录中的陈述都是真实的，也不能证明存档从第一个事件开始，更不能证明时间戳反映了真实时间。团队把这些不同问题统称为“完整性”时，反而会削弱自己的审计结论。它们是不同的主张，审计员也应分别要求不同的证据。

## 密文可以携带完整性轨迹

加密审计日志可以支持离线验证，因为验证器无需理解一条记录，只要对其保存的字节进行哈希即可。它只需要一种明确的记录格式，以及一条将每条记录与前一条记录绑定起来的规则。

一个简单的概念链如下：

```text
record_1 = Encrypt(event_1)
link_1   = H(record_1)

record_2 = Encrypt(event_2)
link_2   = H(link_1 || record_2)

record_3 = Encrypt(event_3)
link_3   = H(link_2 || record_3)
```

`H` 是密码学哈希函数。`||` 表示字节拼接，不是文本连接。生产格式必须明确规定确切的字节、排列顺序和长度编码。如果一个实现对包含换行的内容进行哈希，而另一个实现省略了换行，两者产生差异的原因就只是格式处理，而不是证据本身。

验证器按顺序读取导出的密文记录。对每条记录，它根据此前接受的链接和当前密文字节，计算下一个预期链接。然后，它将计算结果与当前记录或下一条记录中保存的链接进行比较，具体取决于格式定义。不匹配意味着收到的序列不符合链规则。

加密和链式连接解决的是两个不同的问题。加密可以阻止审查人员、备份操作员或取得导出文件的人读取请求 URL、命令参数、响应正文、名称或其他敏感细节。链则可以检测受保护字节是否发生变化。没有链的加密日志能保护机密性，却无法让审查人员区分未改动的存档和悄悄删除过记录的存档。在加密前对明文进行哈希可以检测某些变化，但往往会带来不必要的格式陷阱，而且本身不能为审查人员提供一条可检查的连续序列。

因此，只看到密文的审计员可以做出一个精确的表述：“根据这条链规则和这个锚点，这些加密记录在所提供的序列中没有发生改变或移动。”这个结论很有价值。不要把它夸大成关于隐藏记录含义的结论。

## 有效链证明连续性，不证明真实性

哈希链证明的是一种范围很窄的完整性属性：后续链接依赖于此前保存的字节。它不能证明软件准确记录了某项操作，不能证明所有操作都被记录，也不能证明员工没有制造一段干净却误导性的历史。

例如，一个构建代理发送部署请求。记录器可以在网络调用前记录请求，也可以在网络调用后记录，还可以只在收到成功响应后记录。这三种做法都能产生完美的链，但它们回答的是不同问题：

- 调用前记录显示有人尝试执行某项操作。
- 调用后记录可能显示应用程序到达了自己的完成节点。
- 服务提供方的回执可能显示远程服务接受了请求。
- 网络捕获可以显示通信流量，却未必能识别背后的用户意图。

链无法解决这种语义差距。每条记录声称什么，由应用程序决定。审计设计必须清楚说明这些含义，包括失败、拒绝、重试、取消和部分响应是否都会生成记录。

事故发生后，这种区别尤其重要。有人会问：“代理删除代码仓库了吗？”一条解密后的本地记录可以说明操作网关发送了删除请求，但单凭它无法证明服务提供方执行了请求。服务提供方的事件记录可能回答后一个问题。反过来，服务提供方的事件可以证明删除发生，却未必显示是哪一个本地进程发起了请求。调查人员需要关联多种证据，而不能把一份日志当成完整故事。

代理身份也存在同样的问题。一条记录可以标识进程、代码签名机构、会话或用户批准，而这些身份代表不同的行为主体。如果记录写的是“代理会话 42”，审计员不能在没有其他记录将 Alice 的批准与该会话绑定的情况下，把它改写成“Alice 执行了操作”。链保存的是字节，不会纠正对这些字节的过度解读。

## 第一个可信锚点决定证明能追溯多远

在审计员持有一个不会与存档一起被日志写入方改写的锚点之前，链没有真正有意义的起点。初始链接、检查点摘要或签名承诺都可以承担这个角色。没有锚点，控制存储的攻击者可以删除整份日志，从新的第一条记录开始生成一条新链，再交出一份完全一致的替代存档。

这是内部设计中最常见的错误。团队对一个文件运行验证器，然后得出文件完整的结论。验证器只证明了文件内部自洽。要证明完整性，必须事先承诺历史中预期的某个位置。

实用的检查点应包含足够的信息，让未来的审查人员能够识别这项主张。至少应保存：

```text
log identity: agent-actions-prod
checkpoint sequence: 18427
checkpoint digest: 7f...c2
record format version: 3
hash algorithm: SHA-256
checkpoint captured by: release-control process
checkpoint captured at: 2025-03-08T18:20:00Z
```

上面的摘要只是示例。真实证据记录需要完整摘要、确切的字节编码，以及日志存储之外的持久副本。检查点应保存到具有不同故障边界和访问边界的系统中。源代码管理提交、限制访问的工单附件、签名发布工件、只追加存储，或由外部审计员持有的记录都可以提供帮助，前提是能够改写日志的人无法悄悄改写所有检查点。

检查点还让审计员可以验证一个区间，而不是整个存档。如果审计员信任序列 18,427 的检查点，并收到直到序列 19,100 的记录，验证器就可以检查后面的片段是否源自这个可信检查点。这样，证据主张就被限制在明确的时间窗口内，比假装日志自安装以来一直保持无间断历史更可靠。

检查点无需暴露记录内容。承诺中可以只包含摘要、序列号、格式版本和日志标识。这使外部锚定与加密记录兼容，也避免了一种糟糕的做法：仅仅为了让另一个团队确认存档存在，就导出敏感的审计细节。

## 时间字段在被独立方绑定前都只是主张

加密记录中的时间戳可以按照写入方的时钟排列事件，但单凭它无法证明事件在外部世界何时发生。控制机器时钟、进程或日志格式的人，可以在有效链中写入虚假的时间值。

团队经常把序列与时间混为一谈。链可以证明记录 108 按链规则排在记录 107 之后，却不能仅因为记录中写着 09:17 UTC，就证明记录 108 在这个时间发生。即使是单调计数器也有局限：它能显示一条日志中的顺序，却不能说明两条记录之间经过了多少物理时间。

如果审计需要可信时间，应将检查点绑定到写入方无法控制的外部来源。独立时间戳机构、远程服务回执或由另一团队管理的事件收集器都可以增加这类证据。每种方式提出的信任主张不同。独立时间戳表示另一个主体最迟在指定时间看到了某项承诺。远程服务回执表示该服务观察到某个请求或结果。它们都不会凭空让本地事件文本变得真实。

RFC 3161 描述了一种时间戳协议，其中时间戳机构会返回一个针对消息印记的签名令牌，消息印记通常是哈希值。对于加密日志，消息印记很有用。机构可以对检查点摘要进行时间戳处理，而无需接收加密记录或解密密钥。它的限制同样值得记住：令牌将摘要绑定到机构声明的时间，但不会检查隐藏记录、验证记录语义，也不会证明提交的摘要代表完整存档。

时钟问题也会造成并非恶意的验证争议。工作站可能设置了错误的时区，进程可能写入本地时间而不是 UTC，或者操作员导出文件时使用了与事件顺序不同的排列。应保存清晰的时间表示，但要将链的序列号与任何时间主张分开。审查时应先明确需要证明的是什么：顺序、大致运行时间，还是经过独立见证的时间。

## 离线验证必须保留原始证据字节

离线验证器检查的是字节，不是便于人阅读的显示结果。审计团队打开导出文件并在编辑器中保存、转换换行符、重新序列化 JSON、复制时截断文件，或以错误顺序合并片段，都可能破坏原本有效的证据。

应把收到的导出文件视为证据对象。在任何人查看之前，将它逐位复制到受控存储中。记录其来源、接收时间、经手人员，以及收到文件的摘要。这些处理记录不会让有缺陷的来源变得可信，却能避免审查过程自身增加歧义。

验证流程应将收集、完整性检查和内容访问分开：

1. 保存加密导出文件，并为证据登记册计算文件摘要。
2. 通过记录在案的独立渠道获取相关可信检查点，不要从同一份导出文件中的说明获取。
3. 使用未改动的工作副本运行验证器，并保留退出状态和输出。
4. 如果检查通过，再决定审查是否需要解密。如果需要，则通过单独流程授予访问权限。
5. 如果检查失败，停止编辑，同时保留失败副本和所声称的检查点。

第三步需要保存命令行产物。在安装了 Sallyport 命令行工具的机器上，审计员可以运行：

```sh
sp audit verify
```

该命令会离线验证加密哈希链，无需保险库密钥。应将工具版本、完整命令、工具若支持导出路径时的输入文件标识，以及终端输出一并保存到案件记录中。单独的截图是较弱的证据，因为它隐藏了可执行文件、参数和输入来源。

这项检查应在任何解密请求之前完成。如果链失败，打开记录可能有助于诊断来源，但无法把失败的存档变成完整证据。如果链通过，审查团队通常可以先回答一个初步问题，例如导出后历史是否发生变化，同时继续封存操作机密。

## 断裂的链接告诉你从哪里调查，不告诉你是谁造成的

验证在出现不匹配时停止，说明验证器无法根据提供的前置记录和密文推导出保存的链接。它无法指出造成不匹配的行为者。传输过程中的损坏、不完整的导出、解析器错误、格式版本不匹配和蓄意篡改，都可能表现为相同的最初症状。

应从第一处失败的关系开始向前追查。保存最后一条接受的记录、第一条被拒绝的记录、它们保存的链接和预期检查点。然后比较每次交接时的证据副本。如果文件摘要在生成主机与证据存储之间发生变化，应先调查传输或收集过程。如果摘要保持不变，但新导出始终在同一位置失败，则应调查生成应用程序及其格式假设。

失败模式可以缩小可能范围：

- 在第一条提供的记录处不匹配，通常指向错误的检查点、缺少更早的片段，或导出起点晚于预期边界。
- 在末尾附近不匹配，通常指向复制过程中的截断，或有人过早导出而导致的部分写入。
- 每条记录都失败，通常指向格式版本不兼容，或验证器计算出了不同的字节编码。
- 只有一处不匹配，而后续记录在其他情况下都能连接，可能说明某条记录被修改、损坏或省略。

不要为了看看剩余记录能否验证而修复文件。这个练习可能有助于开发人员调试解析器，却会破坏审计结论所需的纪律。保留一份未改动的取证副本。任何诊断副本都应清楚分开，记录其变换过程，绝不能用它替代原始证据。

设计良好的验证器应提供足够细节来支持调查，同时不暴露加密内容。序列号、记录偏移量、预期摘要、实际摘要和格式版本通常就足够了。如果输出会经过工单系统或聊天记录流转，报告应避免直接倾倒密文。没有密钥时密文可能无法读取，但仍应受到控制，也可能在密钥日后泄露时变成敏感数据。

## 删除和截断需要不同的防护

记录中间部分发生变化时，链会让后续链接停止匹配。中间记录被删除时，如果下一条剩余记录引用了验证器没有收到的前置记录，链也可以暴露这一点。但简单链不一定能发现末尾截断。

假设存档包含记录 1 到 500。攻击者删除 451 到 500，然后向审计员提供记录 1 到 450。前 450 条记录可能全部验证通过。链中没有未来记录指向 451，审查人员需要外部预期来证明历史本应继续延伸。

这种预期可以来自序列 500 的检查点、签名的每日数量、知道最后序列号的外部系统，或记录已完成导出范围的保留流程。有用的主张应具体说明：“存档包含截至序列 500 的每条记录。”单凭链只能支持：“提供的存档在序列 450 之前保持一致。”

第一条提供的记录之前发生的删除也有同样问题。如果审查人员收到从记录 200 开始的链，就无法判断记录 1 到 199 是否曾经存在。导出格式应说明它是从创世记录到当前记录的完整存档，还是一个有边界的片段。有边界的片段需要起始检查点。完整存档仍需要可信的创世值或外部承诺，以防攻击者替换整个文件。

记录数量有助于发现意外丢失，但数量本身不够。有人可以替换一条加密记录，而文件仍然保留 500 条记录，数量无法发现这种变化。数量应作为链和检查点旁的辅助证据，而不是替代品。

这正是可检测篡改的轨迹与只写一次存储主张的区别。轨迹提供证据，让审查人员可以发现某些变化。只写一次存储则试图通过访问控制或存储行为阻止变化。强健的审计计划会同时使用两者，并分别进行测试。把其中一个当成另一个的证明，会留下足以让事故审查失效的漏洞。

## 验证器必须了解格式，不能靠猜

密码学算法无法挽救含义不明确的记录格式。验证器需要精确规范，说明写入方如何将事件转换为密文，以及如何把元数据、此前的链接和密文组合成下一个哈希输入。

应避免依赖便于显示的格式来构造哈希输入。JSON 对象成员顺序、空白、Unicode 规范化和可选字段，可能在不同实现中有所不同，即使呈现出的对象相同。如果链覆盖序列化后的 JSON，就应定义规范化序列化方式，并在不同语言实现之间进行测试。更稳妥的做法是对带有明确字段长度和版本字节的二进制封装进行链式处理。

一个最小封装可以包含日志标识、单调递增序列、前置链接摘要、加密算法标识符、密文、认证标签和格式版本。写入方应提供足够的封装信息，让验证器可以拒绝来自另一条日志的记录，而不是因为字节碰巧形成有效链接就接受它。

版本管理需要谨慎。如果版本 2 改变了记录封装，验证器应明确说明它在转换点采用了版本 2 的规则。不要让验证器静默退回到另一种解析器。静默退回会把兼容性功能变成让格式错误的证据获得错误通过的途径。

美国国家标准与技术研究院的出版物 FIPS 180-4 规定了 SHA-2 系列，并定义了对比特序列执行的哈希函数，而不是对应用意图执行的哈希函数。这一看似枯燥的细节带来一个实际警告。完整性属性只附着于确切的输入字节。团队说“我们对事件做哈希”时，往往还没有决定哈希的是 UTF-8 字符串、数据库行、压缩对象，还是加密封装。在决定之前，他们就没有可复现的审计检查。

应使用刻意设计的极端案例测试格式：空字段、非 ASCII 文本、大型密文、被中断的写入、版本边界处的记录，以及在受支持架构之间复制的记录。同时测试各种变更。翻转密文中的一个字节，交换相邻两条记录，删除中间的一条记录，再截断文件。验证器应以可预期的方式失败，并指出预期关系最早断裂的位置。

## 代理日志需要两层证据

自主代理会产生两个相关的审计问题：哪个运行获得了权限，以及哪个操作使用了这项权限。一条合并的时间线可以包含两者，但审查人员应将这些主张分开。

会话记录可以回答连接到操作网关的进程、与该进程关联的代码签名机构、人类何时批准运行，以及操作员何时撤销运行等问题。操作记录则回答网关执行了哪一项 HTTP 或 SSH 请求。一次运行可以有一项授权和许多操作记录。只看到会话轨迹的审查员不能推断所有外部影响，只看到操作记录的审查员也可能错过该进程为何拥有权限。

Sallyport 维护 Sessions 日志和 Activity 日志。两者都来自同一份写入盲化、加密且由哈希链连接的审计日志。这样的安排让审计员可以从两个角度查看同一份受保护历史，同时仍保留一条可验证的完整性链。

“写入盲化”需要准确理解。它表示负责追加审计证据的组件，在正常工作中不应拥有方便地读取和重写旧记录的路径。它不表示日志写入方无法生成虚假陈述。被入侵的代理可以请求有害操作。被入侵的网关也可能在保留数学上有效的链的同时写入错误数据。这种设计减少了一类重写风险，却不能替代软件完整性、会话审批、必要时的操作审批，以及与远程系统证据进行比较。

对于 HTTP 操作，应保留足够的受保护元数据，以区分目标、方法、凭据引用、审批上下文、结果类别和关联标识，同时避免将秘密材料写入记录。对于 SSH 操作，应区分预期主机、命令边界、会话上下文和结果。加密记录可以为获授权的调查人员携带这些细节。离线验证器无需看到它们，也能检查序列是否发生变化。

这种分离让团队可以将完整性证据包交给没有长期生产机密访问权限的安全审查人员，也让事故处理更有条理。先确定收到的证据是否仍然完整，再授予解释事件所需的最小解密权限。

## 审计主张应与可展示的证据相匹配

有用的审计报告会用清晰的语言说明范围。它应指出导出的片段、链格式、验证器版本、检查点来源、验证结果和仍然存在的限制。这样的报告听起来可能没有“日志不可变”那么绝对，却能经受技术审查。

可以使用这样的表述：“验证器接受了序列 18,428 到 19,100 的记录，确认它们构成一条连续的密文链，并且该链源自 release-control process 单独保存的序列 18,427 检查点。”这句话只说明证据支持的内容。如果团队还有签名的外部时间戳，应单独说明。如果团队将选定操作与服务提供方的事件记录进行了比较，也应单独说明。

除非有锚定的末尾检查点或其他独立来源支持，否则不要说“没有任何记录被删除”。当证据只能识别本地进程时，不要说“代理执行了这项操作”。唯一的时钟属于正在接受审查的主机时，也不要说“就在这个准确时间”。这些不是法律上的文字游戏。它们决定了另一名工程师能否在事故混乱、所有人都想要简单答案时复现你的结论。

第一个实际测试很简单：导出一份非生产环境的样本，将检查点保存在导出位置之外，运行离线验证器，然后在副本中删除一条记录，再次运行验证。如果团队无法解释两次结果及其各自的边界，那么它拥有加密和哈希，却还没有一套能在明文访问成为争议焦点时经得起审查的审计实践。
