Android 构建可复现性:APK 指纹、文件级差异与供应链审计
同一份源码、同一套配置构建两次,产物是否应该完全相同?如果 APK 哈希变化,变化来自业务代码、资源压缩、签名、时间戳还是工具链环境?本文给出一套从整体指纹到 ZIP 条目级差异的审计方法。
一、整体哈希不同,只能说明产物不同
对 APK 计算 SHA-256 是必要的交付校验,但它不能解释原因。ZIP 条目顺序、压缩参数、元数据时间、签名区块和构建工具版本都可能改变最终字节。直接把哈希不同判定为"代码被篡改",会产生大量误报。
可复现性审计应分三层:
- 产物层:大小、整体哈希、签名身份;
- 容器层:条目路径、顺序、压缩方式、压缩前后大小;
- 语义层:DEX、资源表、Native 库和配置的实际变化。
二、首先固定构建身份与环境
每个产物都应携带独立的构建 manifest:
json
{
"source_revision": "example-revision",
"dependency_lock_hash": "sha256:example",
"jdk": "example-version",
"android_plugin": "example-version",
"ndk": "example-version",
"build_variant": "release",
"build_options_hash": "sha256:example"
}
比较前先确认输入身份一致。源码相同但依赖锁不同,不属于同输入;环境不同但输出相同则是好现象,却不能据此假设以后始终相同。
三、生成规范化条目清单
不要一开始解压所有大型 APK。先流式读取 ZIP 中央目录,生成清单:
text
path, method, crc32, compressed_size, size, content_sha256
lib/arm64-v8a/libexample.so,...
classes.dex,...
resources.arsc,...
路径需要检测重复项、大小写冲突和异常穿越片段。哈希按未压缩内容计算,可以排除压缩参数差异;同时保留压缩后大小,便于解释包体变化。
规范化报告应按路径排序,统一数字与编码格式,并剔除明确允许波动的展示字段。注意:剔除仅用于比较视图,原始证据不能修改。
四、差异应按风险分类
推荐将变化分成四类:
- 预期业务变化:代码、资源与版本配置和变更单一致;
- 构建噪声:时间戳、无语义顺序或非确定压缩结果;
- 工具链漂移:编译器、打包器、依赖或环境悄然变化;
- 高风险未知变化:新增可执行文件、Native 库、权限或签名身份改变。
风险分类必须引用允许列表的具体规则和负责人,不能用一个全局"忽略差异"开关。允许列表应有到期时间,避免临时豁免永久存在。
五、Native 库要单独审计
.so 文件变化可能来自代码、链接顺序、Build ID、调试段或编译器差异。建议同时输出:
- 架构和 ELF 基本信息;
- Build ID;
- 可加载段哈希;
- 导入、导出符号集合变化;
- 调试信息是否按策略剥离;
- 与符号仓库中对应文件的身份匹配结果。
只比较文件大小很危险:不同内容可能大小相同,正常的调试信息变化也可能造成巨大体积差。
六、签名验证与内容比较要分开
签名回答"谁为这个产物背书、内容是否被改动",可复现性回答"相同输入能否得到相同输出"。二者相关但不等价。
审计报告应分别记录签名证书指纹、验证结果和内容差异。测试签名、正式签名与渠道重签也应作为不同发布阶段处理,不能把重签后的整体哈希与签名前产物直接对比并得出异常结论。
七、CI 中的双构建验证
对高价值发布候选,可以在两个干净工作区执行相同构建:
- 校验源码、依赖锁和环境 manifest 一致;
- 分别生成产物与条目清单;
- 比较整体哈希;
- 不一致时自动下钻到条目和语义层;
- 生成机器可读差异与人工摘要;
- 未知高风险变化阻止晋级。
双构建会增加成本,可只对发布候选、工具链升级或依赖大改执行。日常提交使用单构建加基线对比即可。
八、输出什么才便于审核
一份有效报告至少包含:输入身份、产物哈希、签名状态、变化条目数、按风险分组的明细、允许规则、证据链接和最终判定。报告不要嵌入内部路径、账号或密钥;正式签名材料更不能进入文章示例和普通日志。
结语
可复现构建不是追求一个漂亮的相同哈希,而是控制输入、解释输出,并让任何未知变化都能被定位。把 APK 当成有结构的供应链工件审计,才能同时服务于包体治理、发布质量和安全追溯。