先看结论与判断条件

  • 预编译 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 或供应商包。任何单一字段都不足以替代完整记录。

加固前固定身份还有一个直接价值:后续每一步都能判断输入是否被替换。若上传、解压、依赖合并或人工拷贝后摘要变化,新文件必须建立新的来源记录;不能继承旧审批,也不能把旧版测试回执贴到新文件。版本相同但摘要不同,可能是合法重建,也可能是供应商静默替换,两种情况都需要解释。

预编译 SO 身份字段及其证明力
字段能够回答不能回答缺失处置
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 固定校验值。若业务确需动态选择,也要把当次解析输出变成候选证据,不能根据配置文件推断实际下载结果。

依赖坐标到最终 SO 的映射链
层级主键核对内容典型错配
依赖请求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 字段仍需外部记录支持。将脚本放入接收流水线时,应先固定原始文件,再执行校验,成功后才允许复制到内部制品库和加固输入区。

校验预编译 SO 来源清单和 attestation subject
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 清单和替换审批,再通过御盾中央平台提交。

想用自己的 App 验证?

提交候选包、目标系统和关键业务路径,申请御盾 PoC 与兼容性评估。

继续阅读: SO 加固与 Native 兼容性检查清单