先看结论与判断条件
- 预编译 SO 的身份主键是实际文件摘要,不是文件名、目录名、压缩包名称或口头声明的 SDK 版本。
- 来源记录要覆盖原始获取位置、获取时间、供应商主体、传输方式、审批人和进入内部制品库后的不可变地址。
- 依赖解析报告解释最终为何选择某个组件版本,但必须再映射到 AAR 或 APK 内真实 SO 摘要,不能停在坐标层。
- 构建证明和 attestation 只有在 subject 摘要匹配、predicate 类型正确、签名者可信且策略允许时才可采信。
- 每个 ABI 的 SO 都是独立产物,应分别登记摘要、ELF machine、版本依据和依赖;同名不代表同一次构建。
- 来源验收通过不等于兼容、安全或加固通过,变换后仍要重新绑定候选摘要并执行静态及设备验证。
先定义要证明的不是文件名,而是文件身份
第三方交付物常被命名为 libsecurity.so、libcore.so 或带版本号的压缩包,但名称可以复制,目录可以重命名,版本文本也可能来自人工填写。来源审计真正要回答的是:当前准备进入加固链的这组字节从哪里取得、由谁声明、属于哪个版本和 ABI、经过哪些保管环节,以及现有证明是否明确指向同一个摘要。
最小身份记录应以 SHA-256 等文件摘要为主键,同时保存文件大小、ELF class、machine、SONAME、build ID、容器内路径和获取时原始名称。摘要解决同名异物问题,ELF 字段帮助发现目录与架构错配,容器路径说明它来自哪个 AAR、APK 或供应商包。任何单一字段都不足以替代完整记录。
加固前固定身份还有一个直接价值:后续每一步都能判断输入是否被替换。若上传、解压、依赖合并或人工拷贝后摘要变化,新文件必须建立新的来源记录;不能继承旧审批,也不能把旧版测试回执贴到新文件。版本相同但摘要不同,可能是合法重建,也可能是供应商静默替换,两种情况都需要解释。
| 字段 | 能够回答 | 不能回答 | 缺失处置 |
|---|---|---|---|
| SHA-256 | 是否为同一组字节 | 字节由谁产生 | 不得进入加固 |
| 文件名 | 交付时使用的名称 | 内容与真实版本 | 仅作辅助记录 |
| ELF machine | 目标架构类型 | 是否已在设备验证 | 与 ABI 目录不符即拒绝 |
| SONAME | 动态链接身份线索 | 供应商和构建批次 | 缺失时记录限制 |
| Build ID | 关联符号与构建身份 | 来源主体是否可信 | 无值时增加其他绑定 |
| 供应商版本 | 外部发布语义 | 文件未被静默替换 | 必须绑定摘要 |
建立从外部来源到内部制品库的保管链
获取来源应记录可复核的原始位置。公开仓库可以记录组件坐标、仓库地址和仓库返回的校验信息;供应商门户可以记录下载页面、订单或授权标识;离线交付要记录交付主体、介质、时间和接收人。聊天窗口中的临时附件或来源不明的网盘副本,不能仅凭发送者备注成为生产输入。
下载完成后应在受控环境立即计算摘要,把原始文件以不可变版本写入内部制品库,再从该库向构建和加固系统供给。转存、解压和重新压缩可以改变容器,但内部 SO 摘要不应无故变化。每次传递记录源摘要、目标摘要、操作者或服务身份和时间,形成能够从候选包反查到原始接收事件的链。
传输加密只能保护通道,不能自动证明下载内容就是供应商预期版本。TLS、登录态或专线记录应与发布公告、供应商签名、仓库校验值、attestation 或书面交付清单组合使用。若多个渠道给出不同摘要,应暂停接入并向供应商确认,不要选择看起来更新的一个继续。
| 渠道 | 来源证据 | 文件证据 | 主要风险 |
|---|---|---|---|
| Maven 仓库 | 仓库与组件坐标 | AAR 和 SO 摘要 | 镜像与解析结果漂移 |
| 供应商门户 | 账号范围与下载页 | 原包和拆出 SO 摘要 | 同版本静默替换 |
| 代码托管发布页 | tag、release 与签名 | 资产摘要 | tag 可变或资产补传 |
| 离线介质 | 交付单与人员身份 | 介质和文件摘要 | 复制链不可见 |
| 邮件附件 | 发件域与审批记录 | 附件摘要 | 转发和附件替换 |
| 临时分享链接 | 通常证据不足 | 仅能计算本地摘要 | 来源和有效期难确认 |
把供应商版本声明映射到真实二进制
版本号可能存在于 Maven 坐标、AAR 清单、SDK 文档、头文件宏、导出函数、ELF note 或供应商清单中。审计应列出每个版本信号及来源,明确哪个是供应商承诺的发布版本,哪个只是构建内部编号。多个信号冲突时不能自行猜测优先级,应由供应商给出与文件摘要绑定的解释。
同一 SDK 版本可能为不同 ABI 分别编译,也可能在不改版本号的情况下重新发布。应为 arm64-v8a、armeabi-v7a、x86_64 等每个实际交付文件建立独立记录,再用一个 release group 关联。只登记版本号和 ABI 列表,却不记录每个文件摘要,会让某一架构被替换后仍显示整组已审批。
内部补丁、strip、符号拆分和加固都会产生新字节。原始供应商版本可以继续作为 lineage 元数据,但处理后文件必须拥有新摘要、处理动作、工具身份、输入摘要和输出摘要。不能把加固后 SO 仍标为供应商原始签发字节,也不能因版本号不变就复用处理前的审计结果。
| 信号 | 可信条件 | 常见冲突 | 处理方式 |
|---|---|---|---|
| 供应商签名清单 | 签名主体可信且摘要匹配 | 清单版本与包名不同 | 以声明语义向供应商确认 |
| 仓库组件坐标 | 仓库受控且解析可复现 | 同坐标内容变化 | 锁定摘要并调查镜像 |
| AAR 元数据 | 容器摘要已固定 | 内部字段未更新 | 仅作辅助线索 |
| 导出版本函数 | 接口文档明确语义 | 运行值与发布说明不同 | 暂停接入并取证 |
| 文件名后缀 | 无独立可信力 | 重命名后误导 | 不作为版本依据 |
| 人工表格 | 有审批和文件摘要 | 复制旧行 | 重新核对原始证据 |
依赖解析图要落到 AAR 和 SO 字节
Android Gradle dependency resolution 说明直接与传递依赖会形成解析图,版本冲突和选择规则决定最终进入构建的组件。保存 dependency insight、锁文件、仓库配置和变体信息,可以解释某个 SDK 坐标为何被选中。但解析图仍停留在组件层,不能直接证明其中包含哪一个 SO。
来源门禁需要从解析结果继续向下展开:记录模块坐标与版本,固定下载到的 AAR 摘要,枚举 AAR 内 jni 目录,为每个 ABI 的 SO 计算摘要,再比较最终 APK 中同路径 SO。若合并阶段选择了另一个模块的同名库,最终摘要会暴露差异;仅查看 Gradle 树可能完全看不到替换。
动态版本、未锁定仓库顺序、内容过滤缺失和本地 flatDir 依赖会降低复现能力。生产构建应使用明确版本和受控仓库,并对 resolved artifact 固定校验值。若业务确需动态选择,也要把当次解析输出变成候选证据,不能根据配置文件推断实际下载结果。
| 层级 | 主键 | 核对内容 | 典型错配 |
|---|---|---|---|
| 依赖请求 | group、name、version | 声明范围和变体 | 动态版本漂移 |
| 解析组件 | selected component | 冲突选择原因 | 传递依赖覆盖 |
| 下载制品 | AAR 摘要 | 仓库返回字节 | 镜像内容不同 |
| AAR 内 Native 库 | 路径与 SO 摘要 | 每个 ABI 文件 | 同版不同架构遗漏 |
| APK 合并结果 | 包内路径与摘要 | 真正交付字节 | 同名库被替换 |
| 加固输出 | 新摘要和 lineage | 输入输出关系 | 旧审批错误继承 |
构建证明必须绑定 subject 摘要
SLSA Provenance v1.1 将产物主体、构建者、构建类型、外部参数和依赖材料组织为构建证明。对预编译 SO,最关键的第一步是确认 subject 的名称与 digest 指向当前文件,而不是只看到一份格式正确的 provenance 就通过。摘要不一致时,这份证明属于另一个产物,后续字段无需继续解释。
in-toto Attestation Statement v1 使用 subject 和有类型的 predicate 负载表达声明。类型让验证器知道应按哪套语义读取内容,但格式正确不保证内容真实。门禁还需要验证签名或可信传输、签发主体、证书或密钥信任范围、撤销状态、签发时间和策略允许的 predicate 类型。
来源方无法提供 provenance 时,不应伪造一份内部构建证明代替。内部可以创建接收 attestation,明确记录谁在何时从何处取得哪个摘要、核对了哪些外部证据以及仍缺什么;它证明的是接收和验证动作,不证明供应商怎样构建。证据类型必须诚实,才能避免下游把人工接收记录误读为可复现构建。
| 校验层 | 必须匹配 | 失败含义 | 处置 |
|---|---|---|---|
| 格式 | statement 与 predicate 结构 | 无法可靠解析 | 拒绝 |
| 主体 | subject digest 等于 SO 摘要 | 证明指向其他字节 | 拒绝 |
| 类型 | predicateType 在策略允许范围 | 语义未知或错用 | 拒绝或人工复核 |
| 签发者 | 身份属于受信主体 | 任何人都可写同样内容 | 拒绝 |
| 时间与撤销 | 签发有效且未撤销 | 信任状态不确定 | 暂停 |
| 内容 | builder、材料与参数合理 | 来源链仍不完整 | 补证或限制接入 |
ABI 需要逐文件验证,不能从目录名继承
Android ABIs 说明不同 ABI 具有独立调用约定、寄存器、对齐和设备支持范围。目录名 arm64-v8a 只是打包者的声明,实际 SO 仍要解析 ELF machine 与 class。若 32 位文件被放入 64 位目录,来源版本再完整也不能进入后续处理,因为产物身份与目标架构已经冲突。
多 ABI SDK 的来源记录应为每个 SO 保存摘要与证明,再说明它们是否来自同一供应商 release。不要假设同名库的导出表、依赖、编译选项和修复水平完全一致。某些供应商会在一个 ABI 重建或修补后只替换该文件,因此每架构独立摘要能把变化限制在真实范围。
ABI 身份确认不等于兼容通过。来源审计只证明当前字节与声明的架构和版本链一致;minSdk、动态依赖、C++ 运行库、符号版本、CPU 特性和设备加载行为仍需要单独测试。把两类结论分开,可以避免一份 provenance 被误用成运行验收报告。
用本地门禁校验摘要、ABI 和证明主体
下面的 Python 脚本读取一个本地 SO、一份内部来源清单和一份 in-toto statement。它计算实际 SHA-256,从 ELF header 解析 machine 得到 ABI,要求清单中的摘要、ABI、供应商、版本和获取来源完整,再检查 statement 的 subject 中是否存在同摘要对象。任何输入缺失、格式异常或不匹配都会返回非零状态。
示例没有验证数字签名,因为信任根、证书链、密钥标识和撤销策略必须由实际组织配置,不能在公开代码中伪造通用答案。生产门禁应在 subject 匹配后调用经过批准的签名验证器,并把验证器版本、信任策略和结果纳入回执。未配置签名校验时,结论只能是摘要和声明结构匹配。
脚本不修改 SO,也不运行其代码;它只检查来源链最基础的一致性。清单本身仍需通过受控审批生成,source 字段仍需外部记录支持。将脚本放入接收流水线时,应先固定原始文件,再执行校验,成功后才允许复制到内部制品库和加固输入区。
import hashlib
import json
import struct
import sys
from pathlib import Path
if len(sys.argv) != 4:
raise SystemExit('usage: provenance_gate.py SO MANIFEST STATEMENT')
so_path = Path(sys.argv[1])
manifest_path = Path(sys.argv[2])
statement_path = Path(sys.argv[3])
for path in (so_path, manifest_path, statement_path):
if not path.is_file():
raise SystemExit(f'missing input: {path.name}')
so_bytes = so_path.read_bytes()
if len(so_bytes) < 20 or so_bytes[:4] != b'\x7fELF':
raise SystemExit('input is not a complete ELF file')
if so_bytes[5] not in (1, 2):
raise SystemExit('unsupported ELF byte order')
byte_order = '<' if so_bytes[5] == 1 else '>'
machine = struct.unpack(byte_order + 'H', so_bytes[18:20])[0]
abi_by_machine = {3: 'x86', 40: 'armeabi-v7a', 62: 'x86_64', 183: 'arm64-v8a'}
actual_abi = abi_by_machine.get(machine)
if actual_abi is None:
raise SystemExit(f'unsupported ELF machine: {machine}')
actual_digest = hashlib.sha256(so_bytes).hexdigest()
manifest = json.loads(manifest_path.read_text(encoding='utf-8'))
for field in ('sha256', 'abi', 'supplier', 'version', 'source'):
if not isinstance(manifest.get(field), str) or not manifest[field].strip():
raise SystemExit(f'manifest field is missing: {field}')
if manifest['sha256'].lower() != actual_digest:
raise SystemExit('manifest digest does not match the SO')
if manifest['abi'] != actual_abi:
raise SystemExit('manifest ABI does not match the ELF header')
statement = json.loads(statement_path.read_text(encoding='utf-8'))
subjects = statement.get('subject')
if not isinstance(subjects, list) or not subjects:
raise SystemExit('attestation subject is missing')
matched = any(item.get('digest', {}).get('sha256', '').lower() == actual_digest for item in subjects)
if not matched:
raise SystemExit('attestation subject does not match the SO')
print(json.dumps({'sha256': actual_digest, 'abi': actual_abi, 'version': manifest['version']}))SDK Index 与内部清单承担不同职责
Google Play SDK Index 可提供部分商业 SDK 的采用、权限、可靠性、安全和数据处理指引,适合在接入评审时补充外部背景。它不覆盖所有私有、开源或定制 SDK,也不直接枚举你收到的 AAR 与 SO 摘要。因此即使页面显示某版本信息,内部仍要完成文件级来源记录。
外部目录的版本状态还可能晚于供应商交付或与私有发行渠道不同。评审人员应记录查询日期、页面覆盖的包或 SDK 身份和可适用结论,不把未出现解释为安全,也不把出现解释为当前文件真实。外部索引负责提供线索,内部清单负责绑定候选字节,两者缺一时要明确数据缺口。
依赖升级决策应以实际解析图、供应商公告、文件证明和项目测试共同支撑。来源门禁发现旧版本,可以触发升级评估,但不能在没有兼容证据时直接替换;同样,兼容测试通过也不能弥补来源未知。更新动作应创建新候选和新摘要,从接收、来源验收到加固和运行验证重新走完。
用替换审批和发布回执闭合链路
NIST SSDF 强调在开发与发布流程中保留来源、构建、验证和变更证据。对应预编译 SO,台账应能从最终 APK 内摘要追溯到加固输出、加固输入、内部制品库、原始接收事件和外部证明。每个转换节点记录输入输出摘要、工具或人员身份、时间、原因和审批,链中任一断点都应阻止自动放行。
替换审批必须比较旧新摘要、供应商版本、ABI 集合、动态依赖、证明主体和已知限制。即使供应商声称只是重新签名或重新打包,只要 SO 摘要改变,就不能视作完全相同。可以复用仍然适用的来源主体和版本文档,但必须新增字节级记录,并明确哪些测试需要重跑。
准备接入或加固评估时,应提交原始包与 SO 摘要、供应商及获取证据、版本信号、依赖解析图、AAR 到 APK 映射、attestation 与签名验证回执、ABI 清单和替换审批。申请入口由御盾中央平台统一承接。缺少真实候选证据时只标记待补证,不写来源可信、兼容通过、攻击阻断或性能收益。
事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| 构建证明应绑定产物主体、构建者、构建类型、外部参数与依赖材料。 | SLSA Provenance v1.1 定义构建 provenance 的核心字段和语义。 | provenance 只证明其记录的构建过程,不能单独证明运行时安全、兼容或供应商声明真实。 |
| attestation 应把产物摘要与有类型的声明负载绑定。 | in-toto Attestation Statement v1 定义 subject、predicateType 与 predicate 的声明结构。 | 声明格式正确不保证内容真实,仍需验证签发者、签名、信任策略和撤销状态。 |
| 安全发布需要保存来源、构建、验证和变更证据。 | NIST SP 800-218 SSDF 将安全软件开发与供应链风险纳入组织级实践。 | SSDF 不定义单个 App、SO 或加固产品的具体功能和验收结果。 |
| Android 直接和传递依赖会形成实际版本解析图。 | Android Gradle dependency resolution 说明依赖解析、版本冲突与选择机制。 | 依赖树只能解释组件选择,不能直接证明 AAR 或 APK 中 SO 的最终字节。 |
| SDK 评审可以参考版本采用、权限、可靠性、安全与数据处理信息。 | Google Play SDK Index 说明其提供的 SDK 信息和使用范围。 | 该索引不覆盖所有 SDK,也不能替代内部二进制清单、来源证明和项目测试。 |
| 每个 Android ABI 都有独立的调用约定、寄存器、对齐与支持范围。 | Android ABIs 描述 Android 支持 ABI 的架构与兼容边界。 | 目录名或供应商声明支持某 ABI,不证明对应 SO 已正确构建、打包和运行验证。 |
| 预编译 SO 的来源记录必须以实际文件摘要为主键。 | 工程判断:文件名、版本文本和路径都可以复用,只有摘要能稳定绑定当前字节。 | 摘要相同只证明字节相同,不自动证明供应商身份、授权范围和运行安全。 |
| 加固、strip 或内部补丁产生的新 SO 需要新的产物身份和 lineage。 | 工程判断:任何字节变化都会使旧摘要与旧候选回执失去直接绑定。 | lineage 说明转换关系,不代表转换后的兼容性、防护强度或性能已经通过验证。 |
工程常见问题
文件名带版本号,能否证明预编译 SO 的版本?
不能。文件名可以被任意修改,应把供应商版本声明、原始获取证据和实际 SHA-256 绑定;多个版本信号冲突时要暂停并确认。
Maven 坐标已经锁定,为什么还要计算 SO 摘要?
坐标解释组件选择,不保证镜像内容永远相同,也不能说明 AAR 合并后 APK 采用了哪一个同名库。文件摘要才能绑定真实字节。
有 SLSA provenance 是否就能直接进入加固?
不能直接放行。先核对 subject 摘要,再验证 predicate 类型、签发者、签名与策略,并检查来源、授权、ABI 和内部接收链是否完整。
同一版本的不同 ABI 可以共用一个摘要吗?
不可以。每个 ABI 的 SO 是独立字节,应分别记录摘要、ELF machine、证明和依赖,再用 release group 表示它们属于同一供应商发布。
来源证明通过是否等于第三方 SO 兼容?
不等于。来源验收回答文件是谁、从哪里来和声明为何;装载、符号、运行库、设备和业务路径兼容仍要单独验证。
申请预编译 SO 加固评估要准备什么?
准备原始包与 SO 摘要、供应商和获取记录、版本信号、依赖解析、AAR 到 APK 映射、attestation 验证、ABI 清单和替换审批,再通过御盾中央平台提交。