先看结论与判断条件

  • 归档主键应是交付候选 SHA-256,而不是 versionName、发布日期或文件名;任何重建、重签和重新加固都会产生新的诊断包。
  • R8 mapping 只用于与同一构建匹配的 Java/Kotlin 混淆堆栈,不能处理 Native 地址,也不能修复候选身份错配。
  • Native symbols 必须按 ABI 和 Build ID 索引,并与 SO、APK/AAB 摘要连接;符号化成功仍不等于崩溃根因已经确定。
  • mapping、未剥离符号和加固映射可能暴露内部名称与实现,应使用最小权限、访问审计、加密存储和受控导出,不进入公开交付。
  • provenance 记录构建者、构建类型、参数和材料,attestation 用候选摘要绑定有类型归档声明;格式正确仍需可信签名者和门禁。
  • 保留策略必须覆盖应用支持周期和故障响应需求,并定期用已知样本执行 retrace、Native 符号化和完整性校验。

归档是一份候选诊断包,不是一组同名附件

加固交付通常同时产生 APK 或 AAB、R8 mapping、Native symbols、Build ID 列表、加固配置与处理回执。若它们散落在不同流水线目录,只靠版本号或日期关联,故障发生时很容易拿错 mapping 或符号。错误还原往往会生成貌似合理的函数名,比明确缺失更危险。

归档包应以 candidateSha256 为根身份,包含 applicationId、versionCode、versionName、variantId、签名阶段、证书摘要、构建者、源码提交和生成时间。每个附件保存相对路径、SHA-256、内容类型、生成工具和责任人,任何文件内容变化都必须产生新归档身份。

文件名可以帮助人阅读,但不能作为门禁。`mapping.txt` 在每次构建中都可能同名,`native-debug-symbols.zip` 也可能覆盖旧文件。工程判断是先将候选和所有诊断材料原子写入不可覆盖目录,再生成哈希清单和有类型绑定声明。

交付诊断包的最小组成
对象主要用途身份字段禁止替代
APK/AAB 候选锁定交付对象SHA-256、版本、签名阶段只用文件名
R8 mapping还原 Java/Kotlin 堆栈mapping SHA 与构建Native symbols
Native symbols还原 Native 地址ABI、Build ID、归档 SHAR8 mapping
加固映射连接处理前后位置工具、配置和输入输出摘要通用符号包
绑定声明证明材料属于候选subject digest 与 predicate运行兼容结果

候选摘要是主键,版本与证书只提供上下文

同一个 versionCode 可能因重试、渠道或重新签名产生多个文件,相同源码也可能因依赖、工具链和环境不同产生不同字节。只有候选文件 SHA-256 能锁定当前交付对象。归档目录、清单、证明和恢复测试都应引用同一摘要。

证书摘要与 signingStage 仍然必要,因为本地测试 APK、上传签名 AAB 和商店最终签名 APK 属于不同交付阶段。诊断材料要说明它对应编译产物、上传对象还是设备派生对象。证书身份不能替代文件摘要,也不能证明私钥保管或发布授权。

APK/AAB 内还可能包含多个 ABI、split 或动态模块。Native 符号需要连接具体 SO 和 Build ID,而不是只连接容器。工程判断是形成 candidate 到 module、ABI、SO、symbolArchive 的层级表,查询时从崩溃对象逐级匹配,拒绝模糊搜索。

归档身份字段的职责
字段回答的问题可作主键边界
candidateSha256是哪一个交付文件可以不说明内部加载路径
versionCode属于哪个发布序列不可以可能存在重建
variantId使用了哪种构建组合不可以同 variant 可多次构建
证书摘要由哪个证书身份签名不可以同证书签多个文件
Build ID是哪一个 Native 构建作为 SO 索引不代替容器摘要

R8 mapping 必须与同一构建产生的混淆代码配对

R8 retrace 说明,混淆后的 Java/Kotlin 堆栈需要对应 mapping 才能还原。mapping 反映一次构建中的名称映射,源码相同但 R8 版本、规则、输入顺序或构建环境改变后,映射可能不同。因此不能用上一版本文件尝试还原新候选。

归档应保存 mappingSha256、R8 或构建工具身份、规则摘要、variant、候选摘要和生成回执。恢复校验可以使用受控的已知混淆堆栈执行 retrace,并断言还原到预期类和行信息。测试样本本身要版本化,不能每次人工目测。

retrace 不处理 Native 地址,也不能确认 APK 中实际 Java/Kotlin 代码与 mapping 匹配,除非构建证据把两者绑定。即使还原成功,也只说明名称映射可用,不代表异常根因已经定位、加固兼容通过或线上问题已修复。

R8 mapping 归档与恢复门禁
门禁输入通过条件失败处理
完整性mapping 文件SHA-256 匹配阻断归档
候选绑定构建证明与 APK/AAB同一 subject拒绝近似版本
工具身份R8 或构建工具版本可追溯记录未知
恢复样本已知混淆堆栈retrace 输出满足断言检查映射错配
权限归档访问策略仅授权读取撤销公开链接

Native symbols 要按 ABI、SO 和 Build ID 建索引

Include native symbols 说明发布构建可以生成独立 Native 调试符号文件并上传到 Play Console。符号包用于线上 Native 崩溃的去混淆或符号化,应用交付包仍可保留剥离后的 SO。上传完成要保存平台回执,本地存在压缩包不等于平台已接受。

ndk-stack 需要同一构建的未剥离符号目录与地址信息。归档应逐 ABI 记录 symbolArchiveSha256、SO basename、soSha256、Build ID、NDK/toolchain 和候选摘要。不同 ABI 的地址和二进制不同,不能用 arm64 符号解释其他架构的崩溃。

符号化成功只把地址还原到函数和源码行。空指针可能源于更早的所有权问题,顶部等待帧也可能不是持锁源。Native 归档门禁负责保证“能用正确材料解释地址”,不负责声明根因、兼容结果或保护强度。

Native symbols 的层级索引
层级主字段核验错配风险
候选容器APK/AAB SHA-256交付对象一致其他重建版本
ABI架构名称设备与目录一致跨架构符号化
SObasename 与 SO SHA-256内部文件身份同名库替换
Build IDELF 构建标识崩溃和符号匹配选错同版本符号
符号归档archive SHA-256文件完整且可读取压缩包损坏或覆盖

mapping 和符号是敏感诊断材料,需要独立访问控制

R8 mapping 可暴露原始类和方法名称,未剥离 Native symbols 可能包含函数、源码路径和实现细节,加固映射还可能描述处理前后位置。它们不应进入公开 APK、网页附件、工单明文或无鉴权对象存储。交付给客户或合作方也要按合同和最小必要范围授权。

归档系统至少需要静态加密、传输保护、角色权限、审批、访问日志、下载水印或等效追踪、保留期限和删除流程。密钥不应和归档包放在一起,下载后的临时副本也要纳入控制。本文不规定具体存储产品,门禁只要求责任可复核。

完整性与机密性要分开。SHA-256 可以发现文件变化,却不能阻止未授权读取;加密可以保护内容,却不能证明附件属于正确候选。工程判断是同时使用摘要绑定、受控存储和审计日志,并定期检查归档条目仍可访问且未失去密钥。

诊断归档的安全控制
控制保护目标验证证据常见缺口
摘要绑定完整性与候选对应哈希清单不提供机密性
加密存储防止静态泄露密钥和策略回执密钥同目录
最小权限限制读取主体角色与审批共享永久链接
访问审计追踪下载和导出不可抵赖日志本地副本失管
保留与删除控制生命周期策略和执行回执版本仍支持却提前删除

provenance 与 attestation 把归档声明绑定到候选

SLSA Provenance v1.1 使用 subject、builder、buildType、外部参数和 materials 描述构建。候选 APK/AAB 可以作为 subject,mapping、符号生成输入、工具链和规则作为材料或参数。这样归档系统能核对诊断文件来自哪个受控构建,而不是只信流水线目录名。

in-toto Attestation Statement v1 使用 subject digest、predicateType 和 predicate 组织有类型声明。可以定义归档 predicate,列出 mapping 摘要、逐 ABI symbol archive、Build ID、保留责任和访问策略。候选摘要写入 subject 后,声明不易被复制到另一个同版本文件。

证明格式本身不保证内容真实。仍需验证签名者是否有权声明、文件摘要是否与归档一致、构建者是否受信、时间是否合理以及 predicate 是否完整。加固输出、商店派生 APK 与上传 AAB 若为不同文件,应各自建立明确关系,不能用一个 subject 覆盖所有对象。

归档证明的关键绑定
证明字段绑定对象核验问题不能推出
subject digest交付候选声明属于哪个文件该文件已发布
builder构建者身份是否由认可流程生成构建实现无缺陷
materials源码、规则和工具输入是否可追溯运行兼容
predicateType归档声明类型审查语义是否明确内容自动真实
predicatemapping、symbols 和责任附件是否完整根因已定位

保留责任和恢复演练决定归档是否真正可用

NIST SP 800-218 SSDF 提倡保留来源、构建、验证和变更证据,并管理供应链风险。mapping 与符号归档应明确生成者、审核者、保管者、故障响应者和删除批准者。没有责任人时,文件即使存在也可能无人知道如何恢复或授权。

保留期限应覆盖应用支持周期、渠道回滚窗口、法务与故障响应需求。不能只保留最新版本,因为仍有用户运行旧版本;也不应无限保留无主敏感符号。工程判断是按支持状态驱动保留,版本退役后由批准流程执行可证明删除。

恢复演练要真实读取归档,而不是检查目录列表。选择受控样本,验证候选摘要、执行 retrace、按 ABI 匹配 Build ID、运行 Native 符号化并核对预期结果;同时确认访问审批和审计日志生效。任何一步失败都说明归档不可用,应在发布前修复。

  • 生成、审核、保管、响应和删除责任明确
  • 保留期限覆盖仍受支持的历史版本
  • 退役版本通过批准流程删除
  • 恢复演练使用真实归档文件
  • R8 retrace 有版本化已知样本
  • Native symbols 按 ABI 与 Build ID 抽查
  • 访问审批和审计日志同时验证

用只读校验器检查候选、mapping 和符号归档绑定

下面的 Python 示例读取归档根目录和 `archive-manifest.json`。清单声明候选、R8 mapping、逐 ABI Native symbols 以及 binding。校验器拒绝绝对路径、目录逃逸和符号链接,重新计算所有文件 SHA-256,并检查 binding 中的 subject、mapping 和符号摘要是否与实际文件一致。

每个 Native symbol 条目必须有安全 ABI 名称、十六进制 Build ID、唯一 ABI 与文件摘要。脚本不解析符号内容,也不声称 mapping 与候选语义匹配;语义对应关系仍由同一构建的 provenance 和签名 attestation 证明。任何缺失、重复或摘要错配都返回非零状态。

准备加固交付归档评估时,可整理精确候选、R8 mapping、逐 ABI symbols、Build ID、工具链、证书摘要、provenance、attestation、访问策略和恢复样本,再通过御盾中央平台提交申请。校验通过不等于崩溃根因或兼容结果已确认。

  • 归档根目录和清单路径受限
  • 绝对路径、目录逃逸和符号链接被拒绝
  • 候选与 mapping 重新计算 SHA-256
  • 每个 ABI 和 Build ID 格式有效且唯一
  • 逐 ABI 符号归档重新计算摘要
  • binding 与实际摘要完全一致
  • 哈希通过不冒充符号语义或运行结论
校验交付候选与 mapping、逐 ABI 符号归档的哈希绑定
from pathlib import Path
import hashlib
import json
import os
import re
import sys

if len(sys.argv) != 3:
    raise SystemExit(2)
archive_root = Path(sys.argv[1]).resolve()
manifest_path = Path(sys.argv[2]).resolve()
if not archive_root.is_dir() or not manifest_path.is_file() or archive_root not in manifest_path.parents:
    raise SystemExit(2)
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
if manifest.get("schema") != "delivery-diagnostics-archive-v1":
    raise SystemExit(2)

def safe_file(relative_path):
    if not isinstance(relative_path, str) or not relative_path or os.path.isabs(relative_path):
        raise SystemExit(2)
    unresolved = archive_root / relative_path
    resolved = unresolved.resolve()
    if archive_root not in resolved.parents or unresolved.is_symlink() or not resolved.is_file():
        raise SystemExit(2)
    return resolved

def digest(path):
    value = hashlib.sha256()
    with path.open("rb") as stream:
        for chunk in iter(lambda: stream.read(1024 * 1024), b""):
            value.update(chunk)
    return value.hexdigest()

def verified_file(record):
    if not isinstance(record, dict) or not re.fullmatch(r"[0-9a-f]{64}", str(record.get("sha256", ""))):
        raise SystemExit(2)
    path = safe_file(record.get("path"))
    actual = digest(path)
    if actual != record["sha256"]:
        raise SystemExit(3)
    return actual

candidate_sha = verified_file(manifest.get("candidate"))
mapping_sha = verified_file(manifest.get("mapping"))
symbols = manifest.get("nativeSymbols")
if not isinstance(symbols, list) or not symbols:
    raise SystemExit(2)
seen_abis = set()
symbol_digests = {}
for symbol in symbols:
    abi = str(symbol.get("abi", ""))
    build_id = str(symbol.get("buildId", ""))
    if not re.fullmatch(r"[A-Za-z0-9_-]+", abi) or abi in seen_abis or not re.fullmatch(r"[0-9a-fA-F]+", build_id):
        raise SystemExit(2)
    seen_abis.add(abi)
    symbol_digests[abi] = verified_file(symbol)
binding = manifest.get("binding")
if not isinstance(binding, dict):
    raise SystemExit(2)
if binding.get("subjectSha256") != candidate_sha or binding.get("mappingSha256") != mapping_sha:
    raise SystemExit(3)
if binding.get("nativeSymbolSha256ByAbi") != symbol_digests:
    raise SystemExit(3)
report = {"status": "archive-binding-valid", "candidateSha256": candidate_sha, "mappingSha256": mapping_sha, "nativeSymbolSha256ByAbi": symbol_digests, "boundary": "hash binding does not prove symbol semantics, root cause, or runtime compatibility"}
print(json.dumps(report, ensure_ascii=False, indent=2))

事实依据与适用边界

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

本文判断事实或工程依据适用限制
混淆后的 Java/Kotlin 堆栈需要与同一构建产生的 mapping 文件配对还原。R8 retrace 描述使用 mapping 还原 R8 混淆堆栈的流程。retrace 不处理 Native 符号,也不能修复错误的候选包身份。
Android 发布构建可以生成独立 Native 调试符号文件并上传到 Play Console。Include native symbols 描述 Native 调试符号生成和上传方式。符号归档仍须绑定版本、ABI 和候选摘要,本地存在不等于平台已接受。
Native 崩溃还原需要同一构建的未剥离符号目录和地址信息。ndk-stack 描述 tombstone 或 logcat 使用 Native symbols 的还原流程。符号化成功不证明崩溃根因已经定位或修复完成。
供应链声明可以用产物摘要和有类型 predicate 绑定归档内容。in-toto Attestation Statement v1 定义 subject、predicateType 和 predicate。声明格式不保证内容真实,仍需可信签名者和独立门禁。
构建证明可以绑定产物、构建者、构建类型、外部参数和依赖材料。SLSA Provenance v1.1 定义 subject、builder、buildType、parameters 与 materials。provenance 记录构建关系,不能单独证明运行时安全性或归档可恢复。
安全发布应保留来源、构建、验证和变更证据,并管理供应链风险。NIST SP 800-218 SSDF 提供组织级安全软件开发实践。SSDF 不定义具体加固功能、符号格式、保留年限或产品能力。
交付诊断归档必须以候选 SHA-256 为主键绑定 mapping 和 Native symbols。工程判断:版本号、日期和文件名可能重复,重建或重签会产生不同二进制。哈希绑定证明文件身份,不自动证明 mapping 或符号语义正确。
归档完成必须通过 retrace、Build ID 匹配和 Native 符号化恢复演练。工程判断:目录存在和摘要正确不能证明工具、权限、密钥和符号内容未来可用。恢复演练验证诊断能力,不等于特定崩溃根因或运行兼容已确认。

工程常见问题

mapping.txt 按 versionCode 命名后归档是否足够?

不够。同一 versionCode 可能重建或重签,应使用候选 SHA-256 作为主键,并保存 mapping 自身摘要与构建证明。

R8 mapping 可以还原 Native 崩溃吗?

不能。R8 mapping 处理 Java/Kotlin 混淆名称,Native 地址需要匹配 ABI、SO、Build ID 和未剥离符号材料。

Native symbols 上传 Play 后还需要内部归档吗?

需要保留受控副本、上传回执和候选绑定,以支持其他渠道、平台不可用时的响应以及恢复验证。

哈希清单通过是否说明符号一定可用?

不能。哈希验证完整性,仍需用已知样本执行 retrace、按 Build ID 匹配并运行 Native 符号化。

mapping 和未剥离符号可以放在公开下载页吗?

不应默认公开。它们可能暴露内部类名、函数和源码路径,需要最小权限、加密存储、审计和受控导出。

申请交付诊断归档评估前要准备什么?

准备候选 APK/AAB、mapping、逐 ABI symbols、Build ID、工具链、证书摘要、provenance、attestation、访问策略和恢复样本,再从御盾中央平台提交申请。

想用自己的 App 验证?

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

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