先看结论与判断条件
- 归档主键应是交付候选 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、归档 SHA | R8 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 匹配,除非构建证据把两者绑定。即使还原成功,也只说明名称映射可用,不代表异常根因已经定位、加固兼容通过或线上问题已修复。
| 门禁 | 输入 | 通过条件 | 失败处理 |
|---|---|---|---|
| 完整性 | 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 归档门禁负责保证“能用正确材料解释地址”,不负责声明根因、兼容结果或保护强度。
| 层级 | 主字段 | 核验 | 错配风险 |
|---|---|---|---|
| 候选容器 | APK/AAB SHA-256 | 交付对象一致 | 其他重建版本 |
| ABI | 架构名称 | 设备与目录一致 | 跨架构符号化 |
| SO | basename 与 SO SHA-256 | 内部文件身份 | 同名库替换 |
| Build ID | ELF 构建标识 | 崩溃和符号匹配 | 选错同版本符号 |
| 符号归档 | 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 | 归档声明类型 | 审查语义是否明确 | 内容自动真实 |
| predicate | mapping、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 与实际摘要完全一致
- 哈希通过不冒充符号语义或运行结论
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、访问策略和恢复样本,再从御盾中央平台提交申请。