额度提高了,却没人经手
一句话总结
审计追踪不是把值存下来,而是把记录留成日后可以争辩的形态。用只追加的记录取代原地 UPDATE,通过规范序列化让同一个事件始终得到同样的字节,再用哈希链和密钥赋予它“中途动手脚就会暴露”的性质。
为什么需要它
在银行,审计应对变得困难的时刻,通常不是值出错的时候。值是对的,却没有人能说明这个值是怎么走到这一步的。设想一个授信额度表,是用 UPDATE customer_limit SET limit_amount = ? WHERE customer_id = ? 这样一行来更新的系统。这条语句一执行,旧值就消失了。剩下的只有 updated_at 和 updated_by 两列,而它们也只是最后一次的痕迹,在此之前经过了多少次,无从得知。
在现场,这个问题是这样出现的。月末检查时,额度总额比月初多了 4.8 亿韩元。清点变更申请单,件数对不上。有几件根本没有申请单,有几件申请单上写的批准金额与现在的值不同。这时去问负责人,得到的通常是诚实的回答——“用批处理统一更新了”。那个批处理做了什么,哪里都没有记录。审计中最昂贵的,不是错误的数字,而是无法解释的数字。
美国 NIST 的 SP 800-92 日志管理指南 在谈到日志时反复强调的,也是同一件事(全文 PDF)。日志不是生成出来就结束了,而是要把保管、保护和验证作为一个整体来设计,完整性得不到保证的日志,作为证据的价值会大打折扣。韩国金融业实务中具体的保存年限因机构而异,本实验不涉及,只专注于记录要怎样做出来,日后才能证明。
工作原理
第一,设置只追加的表。覆盖状态的表(当前额度)和堆积事件的表(审计记录)要分开。当前值是为了快速查询而派生出来的东西,真相是事件的排列。
第二,让事件成为同样的字节。哈希是针对字节计算的,所以 {"a":1,"b":2} 和 {"b":2,"a":1} 虽然是同一个事件,却会得到不同的哈希。RFC 8785 JSON Canonicalization Scheme 把这个问题定成了规范。对象的键要排序,分隔符周围的空白要去掉,并以 UTF-8 序列化。在 Python 里,打开 json 模块 的 sort_keys=True、separators=(",", ":")、ensure_ascii=False 这三项,对大多数实际的 payload 都能得到同样的结果。不过,RFC 8785 按 UTF-16 代码单元定义键的排序,而 Python 按码点排序,所以如果用 emoji 这样的辅助平面字符作键,就会出现偏差。实际工作中,把键限制为 ASCII 更安全。
第三,用链把它们连起来。每个条目都包含前一个条目的哈希作为 prev_hash,再把自己的内容和 prev_hash 一起做哈希,得到 entry_hash(hashlib)。如果修改了中间的一行,这一行的 entry_hash 就会对不上;如果重新计算这一行的哈希再塞回去,后一个条目的 prev_hash 就会对不上。链并不是要让它“无法修改”,而是要让它一改就暴露。
第四,在链上再加一把密钥。只有哈希链的话,能访问全部记录的人可以从头到尾重新计算,把整条链整个换掉。用 hmac 模块 为每个条目生成 MAC,并把密钥放在与记录不同的地方,不知道密钥的人就无法重新计算。如果密钥和记录在同一个 DB 里,这种性质就完全消失了。
app_log(원본) ──▶ canon(사건) ──▶ sha256 ──▶ entry_hash ──┐
▲ │ prev_hash 로 다음 항목에
└──────── prev_hash ◀────┘
entry_hash + prev_mac ──▶ HMAC(열쇠) ──▶ entry_mac
在现场相遇的样子
最常见的失败是没有关联 ID 的日志。一次额度变更,会依次经过柜面应用、额度服务、核心批处理和审计中继,而每个系统都只记下自己的事件。事后问“这次变更是从哪里开始的”,就只能用肉眼把时间相近的几行拼起来。如果给每个请求加上 ID,并让它贯穿全程,调查时间就能从以小时计缩短到以分钟计。反过来,如果中途有一个系统把这个 ID 丢掉了,从那一点之后就又只能靠猜。
第二种,是原封不动地相信从合作方拿到的导出件。链接在一起,并不意味着已经验证过。把收到的文件从头重新计算一遍,就会暴露出只改了内容的位置,以及整条记录都缺失的位置。这两种情况的症状不同。内容被改,这一行的哈希就会对不上;条目被删,下一行的连接就会对不上。
实际工作中真正重要的事
- 记录必须能够重新计算。不要只保存判定结果,还要留下计算用到的规则和输入。
- 验证必须是幂等的。同一个文件验证两次,必须得出同样的结论,而且验证不能修改记录。
- 密钥要放在与记录不同的地方,文件权限设为 600。放在同一个 DB 里,链就成了摆设。
- 移交证据时要以包的形式移交。把文件清单、每个文件的哈希、链的头部哈希和验证结果,放进同一份文档。
下一项实验要做什么
亲手做出 Handeul 银行(虚构)的额度快照,用数字揭示没有申请单就变更的件,以及与批准值不一致的件。接着做出规范序列化工具,使其符合评分器每次都会重新打乱的测试向量,并把 400 行日志用哈希链串起来。在合作方的导出件中找出被人动过手脚的位置,用 HMAC 防止重新计算式的伪造,用关联 ID 把事件按请求重新归并,最后提交附有哈希的证据包。