先看结论与判断条件

  • 证据索引必须以候选摘要和证据文件摘要建立双重绑定,文件名、目录与版本号都不能替代内容身份。
  • provenance、attestation、配置、测试、mapping、Native 符号和签名回执的敏感度与用途不同,保留策略不能一刀切。
  • mapping 必须绑定同一次构建,Native 符号还要绑定 ABI、build ID 与最终 SO;错误配对会产生误导性诊断。
  • 活跃证据要按计划重新计算摘要并验证可读性,备份和恢复副本也必须接受同样校验,不能只验证索引存在。
  • 到期不等于立即删除;诉讼保全、安全事件、未关闭缺陷或仍在支持的版本可以触发冻结,但必须有批准和复查日期。
  • 销毁必须同时处理主存储、缓存、备份和临时导出,并生成不包含客户数据的销毁回执,证明对象、时间、方法和执行责任。

保留策略从证据索引开始,而不是从文件夹开始

加固交付会产生输入输出包、候选摘要、来源证明、配置摘要、签名回执、静态报告、设备测试、mapping、Native 符号和事故材料。只按项目名放入共享目录,过几个月就难以确认某份报告属于哪个候选、文件是否被替换、谁可以读取以及何时可以销毁。

证据索引应为每项材料分配 evidence_id,记录 evidence_type、candidate_sha256、evidence_sha256、生成者、生成时间、工具版本、访问级别、保留到期日、状态、存储位置和责任人。候选摘要是关联主键,证据文件摘要证明索引仍指向同一内容。

索引与文件必须双向可查。通过候选摘要可以列出全部构建、保护、签名和测试材料;通过 evidence_id 可以反查候选与适用结论。没有候选绑定的孤立文件不能补入交付回执,除非经过来源调查、重新计算摘要并由责任人批准关联。

证据索引的最低字段
字段用途校验缺失后果
evidence_id稳定索引全局唯一无法追踪修订
evidence_type决定策略允许类型集合无法分级
candidate_sha256绑定交付候选与候选实物一致证据孤立
evidence_sha256绑定证据字节定期重算无法发现替换
access_class控制读取和导出角色授权泄露风险
retain_until触发复查或销毁日期与冻结状态无限保留或误删

按用途和敏感度划分证据类别

候选摘要、公开来源 URL 和通用测试结论通常敏感度较低,但完整 APK、IPA、SO、保护配置、mapping、未剥离符号、崩溃日志和设备数据可能暴露商业代码或用户信息。访问级别应依据内容本身设定,不能因为都属于同一个交付批次就放在同一共享权限下。

一个可执行分级可使用 public、internal、restricted 和 highly-restricted。公开材料仍需完整性校验;内部材料限制组织成员;受限材料只向项目和审计角色开放;高度受限材料包括密钥相关回执、完整 mapping、Native 符号和含敏感上下文的诊断附件,要求强身份、审批和导出审计。

证据最小化优先于无限加密保存。日志先脱敏,报告只保留必要字段,公开说明不放客户包名、内网、令牌、账号或源代码。若完整原件必须用于事故调查,应在受控库保留原件摘要,并生成脱敏派生件供日常评审,两个对象分别登记。

证据类型与建议访问等级
证据主要用途典型敏感度访问控制
候选摘要清单绑定交付物内部项目与审计可读
provenance 与 attestation来源和处理链内部或受限验证角色可读
保护配置复现处理范围受限安全与供应商受控访问
设备测试回执兼容与路径证据受限脱敏后评审
R8 mappingJava 或 Kotlin 还原高度受限按事故审批读取
Native 符号Native 崩溃还原高度受限ABI 和候选范围授权

mapping 必须与同一次构建永久配对

R8 retrace 依赖混淆构建产生的 mapping 还原 Java 或 Kotlin 堆栈。版本号相同并不足以匹配;重新构建、规则变化、依赖变化或加固处理都可能改变名称映射。索引应绑定输入和最终候选摘要、构建标识、mapping 摘要、R8 版本与规则摘要。

mapping 可以按版本支持窗口和事故调查需要保留,但访问应严格限制。日常质量报告不需要公开完整类名映射,只保存符号化结果与 mapping evidence_id。发生崩溃时由授权角色取出指定 mapping,执行 retrace,并把工具版本、输入堆栈摘要与输出摘要写入派生回执。

retrace 不处理 Native 符号,也不能修复候选身份错配。若拿另一次构建的 mapping 还原,输出可能看似合理却指向错误代码。诊断流程要先验证候选与 mapping 关系,配对失败时明确阻塞,不能尝试多个文件后选择最容易解释的结果。

mapping 归档和使用检查
检查必须匹配失败处理不能推断
候选身份APK 或 AAB 摘要停止 retrace仅凭版本可配对
构建身份构建 ID 与配置调查来源同一分支即相同
mapping 摘要索引记录拒绝读取文件名可信
工具版本R8 与 retrace 版本记录差异任意版本输出一致
访问批准事故或调试范围拒绝导出项目成员都可读
派生回执输入输出摘要补齐审计结果无需复核

Native 符号按 ABI、build ID 和 SO 摘要归档

Include native symbols 说明发布构建可以生成独立 Native 调试符号并上传到 Play Console。内部归档不能只保存一个 symbols.zip 和版本号,应列出每个 ABI 的 SO 摘要、build ID、符号文件摘要、strip 前后关系、NDK 与链接器版本以及最终容器摘要。

加固可能改变 Native 代码布局或生成新的 SO。若输出地址不能由输入未剥离符号直接解释,供应商需要提供受控的映射链或与输出匹配的诊断材料。索引要区分原始构建符号、加固处理映射和最终可用于还原的组合,不能把所有文件统称符号包。

Native 符号包含函数和路径信息,应按高度受限材料管理。上传第三方控制台前确认授权、项目与版本,内部仍保留上传对象摘要和回执。删除本地符号不代表控制台副本已经删除;每个存储位置都需要独立生命周期和销毁状态。

Native 符号索引字段
字段绑定对象用途错配风险
ABI目标架构选择正确符号跨架构误还原
SO SHA-256最终库字节锁定地址对象同名库错配
Build ID构建身份关联 tombstone旧符号误用
Symbol SHA-256符号文件完整性复校文件被替换
NDK 与 linker构建环境解释格式差异工具链混淆
上传回执外部存储副本追踪控制台状态误以为已销毁

provenance 与 attestation 保留证明链而非截图

SLSA Provenance v1.1 把产物 subject、构建者、构建类型、外部参数和依赖材料绑定为构建证明。归档时既保存原始声明字节和摘要,也保存签名验证结果、信任策略版本与验证时间。只保留验证成功截图,未来无法重新验证签发者和声明内容。

in-toto Attestation Statement v1 使用 subject、predicateType 与 predicate 表达有类型声明。索引应提取 subject digest 和 predicate type 作为可查询字段,同时保留原文。声明格式本身不保证真实,签发者身份、证书或密钥、撤销状态和策略决定能否采信。

证明的保留期限至少覆盖其所指候选的支持与调查窗口,但法律、合同和行业规则可能要求不同周期。公开文章无法给出统一年限,项目应由安全、法务、发布和数据治理共同批准。期限改变要保留策略版本和迁移记录,不能静默修改旧索引。

供应链证明的归档对象
对象必须保存定期校验限制
Statement原始字节和摘要可解析性与摘要格式不保证真实
Subject名称和 digest候选是否存在不证明运行安全
Predicate类型和负载策略仍支持语义随类型变化
Signature签名与证书链信任和撤销状态密钥可能轮换
Verification receipt工具和策略版本回执完整性旧结论可能过期
Materials输入依赖标识引用可访问性外部源可能消失

签名回执与候选归档要保持最后写入顺序

Android apksigner 可以签名并验证签名方案覆盖与证书信息,同时要求 APK 签名后不再修改。证据链应保存签名前候选摘要、签名动作回执、签名后最终摘要、证书身份、验证工具版本和验证输出摘要。正式发布回执必须指向签名后的最终字节。

若签名后又执行 zip 对齐、文件替换、渠道注入或其他修改,之前的验签结果不能继续使用。归档系统应把最终候选设为不可变对象,并让签名验证成为最后写入后的门禁。工具返回成功只能说明被检查文件满足其验证,不证明 keystore 管理和商店流程正确。

签名回执通常不需要保存私钥或 keystore。只记录证书公开身份、签名方案、工具、时间、最终摘要和执行系统身份。任何秘密材料都应由专门密钥体系管理,不得为了证据完整把密钥复制进交付档案。

完整性复校必须覆盖文件、索引和恢复副本

活跃证据应按风险设定复校周期,重新读取文件并计算摘要,检查索引字段、对象权限、存储状态和候选关联。仅查询对象存在或比较文件大小不能发现静默替换。复校失败立即冻结相关候选证据,避免事故发生时才发现材料不可读。

备份也要执行恢复演练。加密备份存在不代表密钥可用、对象可读或索引能重建。抽取一组候选,在隔离环境恢复 provenance、配置、测试、mapping 与符号,验证摘要和关联,再销毁演练副本并保留回执。演练不得使用生产密钥或真实用户数据。

访问复查与完整性复校分开。前者确认谁仍有权限、导出是否合理和离职角色是否移除;后者确认字节和关联未变。一个周期内两项都应有负责人、结果、异常和关闭证据,不能用安全扫描绿灯替代对象级读取。

证据周期性复校清单
检查对象失败信号响应
摘要复算活跃证据文件与索引不符冻结并调查
候选关联artifact digest候选缺失或错配阻止引用
权限复查角色和导出越权或离职账号立即撤销
可读性主存储对象损坏或格式不可读恢复备份
恢复演练备份与索引无法恢复关系修复备份流程
策略有效性期限和冻结过期未处理升级审批

用索引校验器拒绝孤立、过期和伪销毁证据

下面的 Python 脚本接收候选文件和证据索引。它计算候选摘要,要求每项记录绑定该摘要、具备合法 evidence_id、文件摘要、访问级别、保留日期和状态。active 证据必须存在、位于索引目录内且摘要匹配;destroyed 证据必须不存在,并提供可读取且摘要匹配的销毁回执。

脚本将已过保留日期但仍为 active 的对象视为阻塞,提醒责任人决定销毁、延长或冻结。它不自动删除任何文件,也不推断法律保留要求。路径解析拒绝索引目录之外的对象,避免验证器意外读取不在本批次控制范围内的材料。

销毁回执只是执行证明,还要由策略定义谁可以批准、哪些副本受影响和如何抽查。实际系统可增加 legal_hold、review_owner、policy_version 和 backup_locations 字段。示例保持公开安全,不包含客户路径、账号、令牌、内部主机或真实保留期限。

校验证据候选绑定、摘要、期限、访问级别与销毁状态
import datetime as dt
import hashlib
import json
import sys
from pathlib import Path

if len(sys.argv) != 3:
    raise SystemExit('usage: validate_evidence.py candidate.bin evidence-index.json')
candidate_path = Path(sys.argv[1])
index_path = Path(sys.argv[2])
if not candidate_path.is_file() or not index_path.is_file():
    raise SystemExit('candidate or index is missing')
base = index_path.resolve().parent
candidate_digest = hashlib.sha256(candidate_path.read_bytes()).hexdigest()
data = json.loads(index_path.read_text(encoding='utf-8'))
if data.get('candidate_sha256', '').lower() != candidate_digest:
    raise SystemExit('index does not bind the candidate')
entries = data.get('entries')
if not isinstance(entries, list) or not entries:
    raise SystemExit('evidence entries are missing')
allowed_access = {'public', 'internal', 'restricted', 'highly-restricted'}
seen = set()
today = dt.date.today()
for entry in entries:
    evidence_id = str(entry.get('evidence_id', '')).strip()
    if not evidence_id or evidence_id in seen:
        raise SystemExit('invalid or duplicate evidence id')
    seen.add(evidence_id)
    if entry.get('candidate_sha256', '').lower() != candidate_digest:
        raise SystemExit(f'{evidence_id}: candidate mismatch')
    if entry.get('access_class') not in allowed_access:
        raise SystemExit(f'{evidence_id}: invalid access class')
    try:
        retain_until = dt.date.fromisoformat(entry.get('retain_until', ''))
    except ValueError:
        raise SystemExit(f'{evidence_id}: invalid retention date')
    status = entry.get('status')
    relative = Path(str(entry.get('path', '')))
    target = (base / relative).resolve()
    if base not in target.parents:
        raise SystemExit(f'{evidence_id}: unsafe evidence path')
    if status == 'active':
        if retain_until < today or not target.is_file():
            raise SystemExit(f'{evidence_id}: active evidence is expired or missing')
        digest = hashlib.sha256(target.read_bytes()).hexdigest()
        if digest != entry.get('evidence_sha256', '').lower():
            raise SystemExit(f'{evidence_id}: evidence digest mismatch')
    elif status == 'destroyed':
        if target.exists():
            raise SystemExit(f'{evidence_id}: destroyed evidence still exists')
        receipt = (base / str(entry.get('destruction_receipt_path', ''))).resolve()
        if base not in receipt.parents or not receipt.is_file():
            raise SystemExit(f'{evidence_id}: destruction receipt is missing')
        receipt_digest = hashlib.sha256(receipt.read_bytes()).hexdigest()
        if receipt_digest != entry.get('destruction_receipt_sha256', '').lower():
            raise SystemExit(f'{evidence_id}: destruction receipt mismatch')
    else:
        raise SystemExit(f'{evidence_id}: unsupported status')
print(json.dumps({'candidate_sha256': candidate_digest, 'entries': len(entries), 'status': 'pass'}))

到期、冻结和销毁要形成同一生命周期

retain_until 是复查触发点,不应让定时任务在无人批准时直接删除。到期后责任人根据版本支持、合同、法规、安全事件、未关闭缺陷和诉讼保全决定销毁、延长或 legal hold。任何延长都记录理由、新日期、批准人和策略版本。

销毁范围包括主对象、缓存、临时导出、工单附件、供应商副本、个人工作区和可识别的备份副本。某些备份无法即时删除时,应记录不可变周期、访问封锁和最终过期时间,不能简单写已删除。mapping 与符号销毁后将失去历史诊断能力,决定前要确认支持窗口和事故是否关闭。

准备证据治理评估时,应提交证据分类、候选索引、摘要清单、访问矩阵、保留依据、复校和恢复回执、冻结记录与销毁证明。申请入口由御盾中央平台统一承接。没有真实材料和存储回执时,只能说明策略已设计,不能声称证据完整、候选可还原或数据已销毁。

事实依据与适用边界

以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。

本文判断事实或工程依据适用限制
attestation 应把产物摘要与有类型的声明负载绑定。in-toto Attestation Statement v1 定义 subject、predicateType 与 predicate 结构。声明格式不保证内容真实,仍需可信签发者、签名和门禁核验。
构建证明应绑定产物主体、构建者、构建类型、参数和依赖材料。SLSA Provenance v1.1 定义构建 provenance 的字段与语义。provenance 只能证明记录的构建过程,不能单独证明运行时安全。
安全发布需要保留来源、构建、验证和变更证据。NIST SP 800-218 SSDF 将安全开发和供应链风险纳入组织实践。组织级框架不规定某项目的具体保留年限、访问等级和销毁方法。
混淆后的 Java 或 Kotlin 堆栈要使用同一构建的 mapping 还原。R8 retrace 说明 mapping 与 retrace 的配合方式。retrace 不处理 Native 符号,也不能修复错误的候选身份。
发布构建可以生成独立 Native 调试符号文件。Include native symbols 说明 Native 调试符号生成与上传方式。符号归档仍须绑定版本、ABI、build ID、SO 和候选摘要。
APK 签名验证后不应再修改被签名文件。Android apksigner 说明签名、签名方案验证、证书和修改顺序。apksigner 成功不证明 keystore 管理、渠道流程和候选归档正确。
活跃证据要定期重算摘要并验证恢复副本。工程判断:只检查索引存在无法发现文件替换、损坏和备份不可恢复。复校通过只证明当次可读和关联,不证明证据内容足以支持所有结论。
销毁必须覆盖主存储、缓存、临时导出和已知备份副本。工程判断:只删除主目录会让其他副本继续暴露或被误用。具体删除周期与方法受存储、合同、法规和 legal hold 约束。

工程常见问题

加固证据是否全部长期保存最安全?

不是。无限保留会扩大泄露和权限风险。应按支持、合同、法规和事故需要分级设期,到期后批准销毁、延长或冻结。

mapping 文件按版本号归档是否足够?

不足。它必须绑定同一次构建的候选摘要、构建标识、工具和规则摘要;相同版本号仍可能产生不同 mapping。

Native symbols.zip 能否覆盖所有 ABI?

要查看实际内容。索引应为每个 ABI 记录 SO 摘要、build ID 与符号文件摘要,不能仅凭压缩包名称判断。

文件摘要一直相同,是否就不用做恢复演练?

仍需要。摘要只能检查现有字节,不能证明备份密钥可用、对象可恢复或候选与证据关系能重建。

保留到期后可以由脚本自动删除吗?

不宜无人批准直接删除。先检查支持窗口、安全事件、合同、法规和 legal hold,再由责任人决定销毁、延长或冻结。

申请加固证据治理评估要准备什么?

准备证据分类、候选索引、摘要、访问矩阵、保留依据、复校与恢复回执、冻结记录和销毁证明,再通过御盾中央平台提交。

想用自己的 App 验证?

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

继续阅读: 御盾 VMP 接入前准备清单