阅读需 8 分钟

保留旧证据的审计格式升级

通过明确的版本、不可变的源字节、迁移测试样本和兼容验证器,规划能够保留旧证据的审计格式升级。

保留旧证据的审计格式升级

只有当调查人员能够使用昨天适用的规则验证昨天导出的证据时,审计格式升级才算安全。新的解析器显示出看似合理的界面、成功完成的数据库迁移,以及绿色的部署结果,都不能证明这一点。它们经常掩盖真正重要的破坏:记录的字节仍然完整,但它的含义、哈希输入或验证器行为已经改变。

把审计格式当作一个拥有漫长尾部的协议。应用可以每周变化,证据不能。一旦有人依靠某条记录来说明谁批准了某个操作、使用了哪项凭据,或代理向外部服务发送了什么,这条记录就需要在写入它的代码消失很久之后,仍然保持稳定的解释方式。

先保留字节,再考虑便利

权威工件是原始记录的字节序列,以及验证它所需的上下文。当前数据库中的解析行只是工作副本。控制面板中显示的 JSON 对象只是视图。两者都不能替代参与签名、哈希链或认证封装的证据字节。

在升级重写字段之前,这个区别似乎有些吹毛求疵。假设版本 1 将 SSH 目标保存为用户提供的字符串:

{"schema_version":1,"event":"ssh.execute","target":"[email protected]:22","command":"uptime"}

版本 2 希望拆分字段,以便按主机和端口筛选:

{"schema_version":2,"event":"ssh.execute","user":"build","host":"prod.example","port":22,"command":"uptime"}

这两条记录描述的可能是同一个操作,但它们不是可以互换的证据。v2 编码器可能会规范化主机名、插入默认端口,或拒绝 v1 接受的目标。如果用第二条记录覆盖第一条,你就对旧事件提出了一个新的主张。这个主张可能是正确的,但仅仅指向重写后的数据,无法证明这一点。

请将以下三类内容分开:

  • 原始证据:被接受进入审计序列时的不可变字节,必须完全一致。
  • 派生表示:用于搜索和显示的索引、解码或迁移形式。
  • 解释说明:解释字段、默认值和版本特定行为的文档化规则。

派生表示可以重新生成,原始证据不能。将原始证据存放在只允许追加的证据包或对象存储中,用摘要为它们寻址,并让每个派生条目都指向对应摘要。如果保留规则要求删除某些内容,就把删除本身记录为一个事件。不要悄悄压缩历史,然后把结果称作升级。

即使日志使用加密记录,这一点仍然适用。加密可以保护内容不被未经授权的人读取,但不能让有损迁移变得无害。验证器仍然必须识别产生结果的具体密文、标头和链规则。

为每条记录提供明确版本

在哈希或签名之前,将明确的 schema_version 放入每条记录。不要从文件扩展名、数据库迁移编号、应用版本,或某个最近新增的字段推断版本。

推断方式在第一次取证导出之前通常都能正常工作。调查人员可能从备份、支持工单或一台不再运行当前应用的机器中复制出一个记录文件夹。周围的上下文已经不完整,但记录仍然存在。自带版本的记录可以告诉验证器需要哪个解码器和哪些规则。

对于规则由你控制的格式,使用一个小整数。将版本 0 保留为无效值,这样缺失字段就不会悄悄变成旧格式。版本字段应承担一个明确而有限的任务:选择记录语法和验证步骤。不要把它变成通用的产品发布编号。

一个持久的封装可以如下所示:

{
  "schema_version": 3,
  "record_id": "01J8X7K5W3H0Q9M6P2R4A1C8ZD",
  "recorded_at": "2026-07-22T14:08:31.482Z",
  "kind": "http.request.completed",
  "previous_digest": "sha256:4a4d...",
  "payload": {
    "method": "POST",
    "authority": "api.example.test",
    "status": 201
  }
}

版本必须属于认证输入。如果记录声称自己是版本 3,但计算摘要时没有包含这个字段,那么能够编辑存储字节的攻击者可能会把验证器引向另一条解释路径。凡是会选择解析器、哈希算法、规范化规则或签名算法的字段,都要包含在受保护字节中。

还要将记录架构版本事件含义版本分开。前者回答的是「如何解析和验证这些字节?」后者回答的是「这个事件在发出时意味着什么?」

例如,将 actor 从自由格式的显示名称改为稳定的进程身份,可能不会改变 JSON 结构,却会改变所作的主张。这不只是架构版本 4,而是语义变化。证据规范需要说明新含义从什么时候开始。字段单位发生变化、时间戳从本地时间改为 UTC,或状态从观察到的响应改为策略决策时,也同样如此。

冻结验证步骤,而不只是字段

如果验证器无法还原用于认证记录的精确字节步骤,版本化架构就是不完整的。字段布局只是这套步骤的一部分。

请为每个版本写明:

  • 接受的记录语法和必需字段;
  • 文本或二进制编码,以及规范化规则;
  • 摘要和签名算法;
  • 使用的域分隔符;
  • 链接规则和创世规则;
  • 对格式错误或未知输入的失败行为。

RFC 8785 解释了为什么 JSON 在执行加密操作前需要确定性表示:普通 JSON 允许同一个逻辑值拥有多种序列化形式,而哈希和签名要求字节保持不变。它的 JSON 规范化方案限制了输入,并以确定性方式对对象属性排序。它还通过格式限制提醒我们,重复属性名和超出支持范围的数字并不是可以忽略的小事。

只有明确指定所采用的配置,这个标准才有用。说「我们对 JSON 做哈希」不是一套步骤。说「我们调用当前运行时的序列化器」更糟,因为运行时升级可能在没有任何有意的审计变更时,改变转义、数字格式或排序方式。

二进制格式也有相同问题。RFC 8949 规定了 CBOR 的确定性编码要求,并指出,如果使用此前的排序约定,就需要明确命名兼容模式。验证器不能安全地假定每个历史生产者对「规范」的理解都相同。

不要构建这样一种新验证器:解析任意 JSON,用今天的库重新序列化,然后对结果做哈希。这种模式会以两种方式破坏旧证据。它可能拒绝旧步骤下有效的记录,也可能依据从未产生原始摘要的新步骤接受一条记录。

相反,应将版本分派放在字节边界附近:

read envelope bytes
  -> identify protected schema_version
  -> select verifier V1, V2, or V3
  -> validate that version's grammar
  -> reproduce that version's authenticated bytes
  -> verify digest, signature, and chain link
  -> decode a display model only after verification

显示模型最后才出现是有意为之。渲染器可以追求易用,验证器不能凭想象补全信息。

未知版本必须默认拒绝

验证器遇到不支持的版本时,必须返回明确的 unsupported_version 结果。它不能把未知字段当作可忽略内容,不能假定使用最新布局,也不能运行通用的备用解码器。

工程师经常对此有所抵触,因为他们希望实现向前兼容。应用读取可选的展示字段时,向前兼容很合适。但在证据验证中,某个看起来可选的字段之后可能会控制认证解释,因此这样做很危险。

使用一种能区分证据失败与工具限制的结果结构:

{
  "record_id": "01J8X7K5W3H0Q9M6P2R4A1C8ZD",
  "schema_version": 4,
  "status": "unsupported_version",
  "verified": false,
  "supported_versions": [1, 2, 3],
  "reason": "Verifier 2.7.0 has no verification recipe for schema version 4"
}

这个结果表达得很准确:工具尚未确认真实性。它没有指控记录遭到篡改,也没有假装记录有效。无效、不完整、不支持的版本和已验证状态,必须在命令输出和 API 中保持区分。

哈希链还带来另一项兼容性要求。不能只根据最终摘要推断链的历史。验证器需要知道第一条记录、记录排序、父摘要编码和检查点格式各自对应的版本规则。这里可以参考证书透明度的思路:RFC 9162 定义了一致性证明,用来说明较早的树是较晚的树的同一前缀,而不是要求审计人员相信一个新报告的根哈希。

你的审计序列可能不使用 Merkle 树,但这个教训仍然成立。当链规则发生变化时,要在边界处证明连续性。创建一个终止 v1 检查点,其中包含最后一个已验证摘要、记录数和版本。让第一条 v2 记录在定义好的字段中认证这个检查点。v2 验证器应先按照各自规则验证两侧,然后再报告这是一条连续历史。

绝不要把旧摘要作为注释或显示字段保存,以此连接两条历史。连接关系必须成为受保护输入的一部分。

迁移应生成派生内容,而不是替换内容

升级后验证导出文件
无需打开保险库,即可离线验证 Sallyport 的加密哈希链审计日志中的密文。

好的迁移会在源证据旁边创建一个新的、可复现的派生内容,并记录足够的来源信息,让其他人能够重新生成相同的输出,再与源证据进行比较。

对于每条迁移记录或每个迁移批次,记录:

{
  "source_digest": "sha256:4a4d...",
  "source_schema_version": 1,
  "migration_id": "audit-v1-to-v2",
  "migration_build": "2.7.0+e31c9f4",
  "output_digest": "sha256:77c8...",
  "migrated_at": "2026-07-22T14:12:09Z"
}

migrated_at 表示派生内容的时间,而不是原始事件的时间。不要覆盖 recorded_at,也不要把生成的 v2 记录呈现得像旧系统发出的记录。与明显的解析器崩溃相比,这个错误在内部调查中造成的混乱更多。

有些迁移无法做到无损。v1 记录可能只有一个 target 字符串,而 v2 要求结构化 URI。如果解析失败或存在歧义,就保留源字符串,并记录明确的迁移状态。不要因为新索引需要结构化值,就凭空创造一个。

例如:

{
  "source_digest": "sha256:4a4d...",
  "migration_status": "partial",
  "derived": {
    "target_raw": "[email protected]:22",
    "host": "prod.example",
    "port": 22
  },
  "unresolved": ["user"]
}

这看起来可能没有完整填充的行整齐,但更加诚实。未来的调查人员可以同时看到旧记录说了什么,以及迁移推断出了什么。

不要采用那种流行的建议,把每条旧记录都交给当前写入器处理,然后称之为升级。它之所以流行,是因为这样可以简化一条代码路径,让报告看起来统一。但对于证据来说这是错误的,因为写入器通常会应用当前默认值、删除已弃用字段并规范化值。生成的输出可以用于搜索,但它是翻译,不是原始证词。

永久测试样本库能捕捉悄无声息的破坏

兼容性是一项测试资产,不是发布说明中的承诺。为每个已发布的架构版本建立证据语料库,并让每个声称支持该版本的验证器构建都运行它。

语料库不能只有少量正常记录。请为以下情况保留字节完全一致的测试样本:

  • 一条普通的有效记录和一条有效的多记录链;
  • 该版本接受的边界时间戳、Unicode 文本、空的可选值和数值上限;
  • 载荷字节发生变化的记录;
  • 父摘要发生变化或序列顺序被重新排列的记录;
  • 格式错误、重复、截断以及未知版本的输入。

保存预期结果,而不只是预期解码对象。测试应断言证据结果和诊断类别。一个把有效记录正确标记为无效的验证器仍然是失败的。一个把格式错误的记录变成通用解析器异常的验证器,虽然失败得不那么明显,但它让调查变得更加困难。

使用清单固定测试样本摘要和预期的验证器行为:

fixture: v1/0007-http-request.json
sha256: 4a4d5f0c...
expect:
  status: verified
  schema_version: 1
  chain_position: 7

fixture: v1/0007-http-request-tampered.json
sha256: 91af2a7d...
expect:
  status: invalid
  error_code: payload_digest_mismatch

然后从多个方向进行测试。

  1. 最旧的保留验证器仍必须验证其原始语料库。
  2. 当前验证器必须验证所有保留的历史语料库。
  3. 候选写入器必须生成当前验证器能够按照新版本接受的记录。
  4. 候选迁移必须保留声明的源摘要,并生成预期的派生内容。
  5. 每个验证器都必须拒绝为不支持的未来版本设计的测试样本。

测试失败时,不要直接重写预期样本。先检查字节、选择的验证器版本和失败代码。测试样本更新应当很少发生,并且像协议变更一样接受审查,同时附上理由,说明这是有意新增的样本,还是证据发生了变化。

在解析器周围加入属性测试,但不要把它与永久语料库混为一谈。随机输入可以发现崩溃和奇怪的边界情况。命名测试样本则保留你已经付出代价才学到的情况,包括删除敏感内容后的真实发布版本记录。

像调查一样测试升级边界

让保险库成为绝对门禁
当硬件控制的保险库在 Mac 上锁定时,拒绝所有代理操作。

最高风险通常在版本之间的边界,而不是某个版本内部。编写一个从升级前开始、升级后结束的场景,然后问自己:一个独立的人员能否解释整个序列?

假设一条链中,v1 记录了代理会话批准、几次 HTTP 调用和会话撤销。版本 2 引入了更详细的请求结果字段和新的校验和编码。测试应从 v1 创世记录开始,追加有效的 v1 条目,创建文档规定的边界检查点,追加 v2 条目,然后导出完整证据包。

预期的验证报告需要清楚显示过渡:

$ audit verify evidence-bundle
verified v1 records: 18
verified v1 terminal digest: sha256:6c12...e98a
verified v1-to-v2 continuity checkpoint
verified v2 records: 6
chain status: verified

现在运行生产升级会造成的失败情况:

  • 删除最后一条 v1 记录,但保留 v2 记录;
  • 修改检查点中的 v1 摘要;
  • 使用一条标记为 v1 版本的 v2 记录;
  • 只导出 v2 片段,然后要求给出完整历史判断;
  • 使用 v2 之前的验证器验证混合证据包。

正确的系统会给出不同答案。前三种情况无效。第四种情况可以在证据包声明起始检查点时,作为部分片段通过验证,但不能声称完整历史已经验证。第五种情况应在报告能够验证的 v1 证据后返回 unsupported_version,前提是命令设计允许部分报告。它不能把整个证据包报告为已验证。

团队通常会在这里发现,日志和控制面板隐藏了源边界。只要界面标记出架构转换,并允许审查人员检查原始封装,将记录合并为一条时间线没有问题。不要让调查人员通过某个字段突然出现,自己猜测格式发生了变化。

让验证器足够小,能够比应用活得更久

区分会话与调用
检查由同一份防篡改加密审计日志支持的会话和活动日志。

审计验证器的依赖和权限都应该少于生成记录的应用。如果读取旧证据需要启动图形应用、连接账户、打开凭据保险库,或下载兼容性软件包,那么你的证据计划就依赖于最不该消失时最可能消失的条件。

分开承担这些职责:

  • 应用写入记录并展示实时活动。
  • 小型验证器读取导出的证据包,选择版本化步骤,并输出机器可读的报告。
  • 渲染器可以将已验证的记录转换为表格和时间线,但不参与真实性判断。

让验证器保持确定性。对于同一个证据包和相同的命令选项,它应返回相同的状态码和报告结构。报告中可以包含验证器发布标识,但不能让这个标识改变证据结果。

对于 Sallyport,在升级运行手册中保留 sp audit verify 是正确的检查方式,因为它可以在离线状态下,直接对密文验证加密哈希链,而且不需要保险库密钥。在修改应用之前,将命令报告保存到未经改动的导出文件旁边,升级后再次验证同一份导出文件。

「离线」这个词需要严格定义。它意味着验证器可以根据现有证据和内置验证步骤,确认链的结果。它不意味着验证器可以还原缺失记录、判断是谁操作了某台机器,或证明用户理解了某张批准卡。设计良好的报告会准确说明它检查了哪些主张。

将证据格式规范与验证器源代码和测试样本一起发布。没有测试样本库的源代码发布,会让未来维护人员猜测兼容性。没有书面步骤的语料库,也会让他们猜测通过的测试究竟反映了有意制定的规则,还是某个实现中的偶然行为。

在第一次紧急情况之前制定停用政策

只有在决定如何处理使用旧格式的证据之后,才能停止支持某种历史格式。这个决定属于安全、法律、运营和调查事件的相关人员,不应成为删除旧软件包时顺带产生的结果。

编写支持表,列出保留的版本、能够读取这些版本的验证器发布版本、预期证据保留期限,以及特殊归档的流程。如果打算停用某个读取器,先提供一个独立的归档验证器,并冻结它的测试样本库。将构建说明和预期校验和与证据文档放在一起。

不要轻易承诺永久支持。算法会老化,操作系统会变化,旧解析器也可能带有安全缺陷。但要保留验证已保留证据的途径。有时这意味着使用只接受本地文件的沙箱归档工具,有时意味着保留一个带有已记录哈希的容器或虚拟机镜像。选择取决于你的环境,必须做到的是,不让未来的调查人员凭记忆重建一套已经消失的工具链。

第一步很具体:导出一小份真实证据包,运行验证器,然后写下命令依赖的每条版本规则。如果你无法描述这套步骤,也无法在测试升级后重现结果,那么你还没有升级计划,只有一个希望旧证据仍然可读的愿望。

常见问题

架构发生变化时,应该直接迁移审计记录吗?

保留原始字节,以及能够理解这些字节的验证器。迁移可以创建方便使用的当前视图,但绝不能替换原始证据,也不能成为验证原始证据的唯一方式。

架构版本和事件语义版本有什么区别?

不是。格式修订改变的是表示方式,事件修订改变的是事件含义。如果把语义变化当成表面变化,旧报告看起来就可能表达它们从未表达过的意思。

审计架构版本应该存在哪里?

在每条签名或哈希记录中加入明确且必需的字段,例如 schema_version。文件名、存储路径或数据库列可以作为有用的元数据,但对于可能被导出或复制的证据来说,它们并不充分。

新的审计验证器能读取升级前创建的记录吗?

旧验证器应该拒绝未知版本,而不是猜测。新验证器应保留仍承诺支持的每种证据格式所需的旧解码器和验证规则。

把旧审计日志改写成新的 JSON 格式安全吗?

只有在原始字节仍然可用,并且明确将迁移标记为非权威操作时才安全。将迁移后的表示保存为派生工件,并记录源摘要、迁移工具版本和清晰的标签。

规范化 JSON 规则能解决审计日志兼容性问题吗?

规范化 JSON 可以避免空白或对象成员顺序等看似无害的序列化差异改变签名字节。但它无法解决字段含义不清、重复字段、精度丢失,或哈希输入发生未记录变化等问题。

审计系统应该永久保留哪些迁移测试?

为每个已发布版本永久保留一个测试语料库,其中包括有效记录、边界情况、格式错误的记录和已知篡改案例。让每个受支持的验证器在持续集成中运行这些测试,并要求每个样本都得到预期结果。

哈希链本身能让架构迁移变得安全吗?

哈希链可以按照自身规则证明序列没有被悄悄改动,但无法证明新的解析器为旧字段赋予了相同的人类含义。因此,必须冻结并测试每个版本的解释规则。

审计格式验证器应该支持旧记录多久?

制定书面的支持政策,并结合调查人员的需求、合同义务和保留期限。停止旧验证器的支持是产品和证据决策,不是工程团队的日常清理工作。

升级后如何验证 Sallyport 审计导出文件?

在应用升级前后,分别对一份未经改动的证据导出文件运行 sp audit verify,然后将两次结果与导出文件一起保留。该命令可以离线验证 Sallyport 的加密哈希链,因此检查过程不依赖打开保险库。

Sallyport

Sallyport 替你的 AI 智能体执行 API 调用和 SSH 命令。密钥留在你 Mac 上的本地密钥库里;每次运行由你批准,每个操作都落入一份密封的审计日志。

© 2026 Sallyport · 依据 Apache-2.0 开源 · Oleg Sotnikov