先看结论与判断条件
- 包名回答更新归属哪个已安装应用,versionCode 回答发布顺序,签名证书回答谁有资格延续该应用,三者缺一不可。
- versionName 是面向用户的显示信息,不能替代 versionCode,也不能作为判断候选新旧和更新连续性的唯一字段。
- 证书摘要必须从最终 APK 的签名验证报告提取,构建配置声称使用生产签名不能证明实际候选确实如此。
- APK SHA-256 摘要负责锁定候选字节,使清单、签名、测试和渠道回执不会被复制到另一个同名文件。
- in-toto 与 SLSA 类型的来源证明可以把产物摘要与构建者、参数和材料关联,但仍需验证签名者及组织策略。
- 发布门禁应先生成身份元组再运行测试;任何重签名、重打包或字节变化都会产生新候选并使旧回执失效。
发布身份是一组不可拆分的事实
团队日常会用“正式包”“某版本”或一个 APK 文件名指代候选,但这些标签都不够精确。文件名可以被复制,versionName 可以重复,构建目录也可能同时存在不同渠道和签名的产物。真正参与 Android 安装与更新判断的是最终应用标识、版本顺序、签名身份和候选字节。验收系统需要把它们合成一个元组,禁止单独替换其中任意字段。
这个元组不是为了制造更多元数据,而是解决证据错配。常见事故是测试团队验证了一个 Debug 签名包,发布团队上传了同 versionCode 的另一个包;或渠道对 APK 重新处理后仍沿用原测试报告。两个文件的界面和版本名可能完全相同,却不是同一发布候选。只要 APK 摘要或证书变化,旧证据就不应继续生效。
建议的最小身份由 package_name、version_code、certificate_sha256、apk_sha256 和 provenance_ref 组成,version_name、签名方案、构建流水线编号与渠道作为附加字段。每份静态扫描、设备回归、审批单、上传回执和回滚记录都引用该 identity_digest。系统只有在元组精确匹配时才聚合结果,避免人工凭文件名拼接。
| 字段 | 回答的问题 | 错误替代 | 变更后的动作 |
|---|---|---|---|
| package_name | 候选属于哪个安装与更新命名空间 | 应用名称或渠道名 | 视为不同应用身份重新验收 |
| version_code | 该候选在更新顺序中的位置 | versionName 或构建日期 | 重新核对升级路径与商店规则 |
| certificate_sha256 | 哪个签名身份授权该候选 | keystore 文件名或配置项 | 新身份需要签名连续性证据 |
| apk_sha256 | 证据对应哪一组精确字节 | APK 文件名或下载 URL | 任何字节变化都生成新候选 |
| provenance_ref | 候选从哪个构建过程和材料产生 | CI 任务显示成功 | 来源变化时重新审计构建链 |
| identity_digest | 如何用单值引用整组事实 | 手工拼接版本字符串 | 重算并使旧回执失效 |
从最终 Manifest 读取包名与版本
Android app manifest 描述应用组件、权限、intent filter、SDK 约束和应用元数据。发布身份所需的包名与版本必须从最终 APK 解码结果读取,而不是从源码中的默认配置推断。构建变体、product flavor、占位符、插件和渠道流程都可能改变最终值。检查者要保存解码工具版本和原始报告摘要,并将结果绑定到同一个 apk_sha256。
package_name 是安装与更新身份的核心之一,但不要与代码 namespace、应用展示名称或进程名混淆。验收时应比较最终 Manifest 报告中的应用标识与允许列表,并同时核对主要组件和权限是否属于预期候选。本文只定义身份元组,不承担组件暴露与权限安全评审;发现 Manifest 其他字段异常时应进入相应专门门禁。
version_code 用于发布与更新顺序,version_name 主要用于向用户展示。业务可以选择任何可读的 versionName 规则,但不能据此判断哪个候选更新。身份 JSON 应把两者分开保存,更新门禁以 versionCode 和平台条件为准。若相同 versionCode 产生不同 APK 字节,流水线应判定为不同候选冲突,而不是覆盖旧文件。
| 观察项 | 可靠来源 | 常见误区 | 门禁规则 |
|---|---|---|---|
| package_name | 最终 APK 的 Manifest 解码报告 | 读取源码默认 applicationId | 必须命中产品允许列表 |
| version_code | 最终 APK 的版本字段 | 用 versionName 判断新旧 | 满足目标轨道的更新顺序 |
| version_name | 最终 APK 的显示版本字段 | 认为显示相同就是同一候选 | 仅作为辅助展示信息 |
| minSdk 与 targetSdk | 最终 Manifest | 从 Gradle 文件推断发布值 | 变化时触发兼容与策略重验 |
| 组件和权限 | 合并后的最终 Manifest | 只看应用模块源文件 | 交由独立安全门禁核对 |
| 报告工具版本 | 受控构建环境 | 不同工具输出混用 | 与报告摘要一起归档 |
| apk_sha256 | 对最终 APK 原始字节求摘要 | 用文件名关联 Manifest | 所有字段必须绑定该摘要 |
从签名验证报告锁定证书身份
Android 发布文档区分应用签名密钥、上传密钥、证书以及 Play App Signing 下的责任。流水线配置写着 release 并不能证明实际 APK 使用正确证书;测试人员也不应通过 keystore 文件名猜测。应对最终 APK 执行 apksigner verify,记录验证状态、签名方案和签名者证书 SHA-256 摘要,再与产品批准的证书集合比较。
证书摘要在元组中表达当前候选的可验证签名身份,不保存私钥、口令或 keystore 路径。若报告出现多个签名者,应按项目签名策略处理并保存有序或规范化集合,不能随便取第一条。证书轮换与 lineage 有自己的兼容证明和平台边界;本文不展开轮换流程,只要求身份元组能引用相应证据,不能把新旧证书当成同一个字符串。
apksigner 文档还要求签名后不要再修改 APK。任何 zipalign、资源注入、渠道重打包或 ZIP 元数据变化都可能使签名失效或至少改变 apk_sha256。正确顺序是先完成全部字节修改,再签名、验证、生成身份元组,随后只做只读检查。若后续字节改变,应丢弃旧元组并从签名验证重新开始。
- 证书摘要来自最终 APK 的 apksigner 验证报告
- 签名方案和全部签名者按项目策略记录
- 只保存公开证书摘要,不记录私钥或口令
- 证书必须命中产品和发布轨道允许集合
- 签名后禁止任何会修改 APK 字节的动作
- 轮换候选引用独立的签名连续性证据
- apksigner 成功不扩大为密钥管理或渠道流程通过
把更新连续性落到元组比较
Android 应用更新需要保持应用标识,并满足签名连续性与可接受的 versionCode 等条件。How Android app updates work 说明平台会检查包、签名和版本关系。发布门禁不应只问“新包能安装吗”,而应将已上线元组、目标候选元组和预期升级路径一起比较,区分全新安装、正常升级、证书轮换和不允许的降级。
元组比较必须使用受信基线。基线来自已发布回执、商店轨道记录或受控归档,而不是开发者电脑上最近的 APK。若线上存在多个轨道或渠道,应分别维护当前 package_name、最大已接受 versionCode、签名身份和候选摘要。把测试轨道基线错当正式轨道,可能导致 versionCode 计划冲突或把错误证书带入发布。
平台更新条件并不等于业务层防重放。即使 versionCode 满足安装规则,服务端仍可能要求最低版本、强制更新或撤销某个候选;反过来,客户端修改本地显示版本也不应改变服务器授权。本文的元组用于精确标识候选和更新链,业务版本策略需要另建可审计规则,避免把两类责任混在一个布尔判断里。
| 场景 | 必须比较 | 允许条件 | 证据输出 |
|---|---|---|---|
| 全新安装 | 包名、签名和候选摘要 | 属于批准产品与轨道 | 安装候选身份记录 |
| 常规升级 | 包名、versionCode、证书连续性 | 同一应用且版本顺序可接受 | 升级前后元组与设备回执 |
| 证书轮换 | 当前证书、新证书与连续性材料 | 平台与轨道均认可轮换关系 | 独立轮换证明引用 |
| 测试转正式 | 轨道、证书和候选字节 | 正式轨道规则全部重新核对 | 正式发布专用元组 |
| 渠道处理 | 处理前后 APK 摘要与签名 | 新候选重新完成所有门禁 | 渠道候选的新 identity_digest |
| 回滚申请 | 历史元组、当前基线和业务策略 | 显式批准且满足平台条件 | 回滚原因与新审计事件 |
| 同版本重构建 | 相同 versionCode 下的 apk_sha256 | 不得静默覆盖既有候选 | 冲突记录与重新编号决策 |
用 APK 摘要把所有证据钉到同一文件
package_name、versionCode 和证书都可能在不同 APK 中重复出现。例如同一版本用同一密钥重新构建,时间戳、资源压缩或依赖字节变化后,前述三项仍然相同。APK SHA-256 摘要用于区分这些候选。它不表达候选好坏,却能保证静态分析、设备测试和上传回执讨论的是同一组字节。
摘要应在最终签名后计算,并贯穿所有系统。测试平台接收文件时重算,发布审批读取同一值,上传完成后从回执或下载样本再次核对。不要依赖客户端上报摘要作为唯一事实,也不要把 URL、对象存储键或文件大小当作摘要替代品。若系统会转封装或由商店生成派生 APK,应分别记录容器和派生产物身份。
身份元组本身也可以规范化序列化后计算 identity_digest。字段顺序、编码、证书集合排序和空值处理必须固定,schema_version 变化时旧验证器应明确拒绝。identity_digest 是方便系统引用的索引,不替代每个字段的原始证据。审计人员仍需查看 Manifest 报告、签名报告和实际 APK 摘要是怎样产生的。
| 标识 | 绑定范围 | 生成时机 | 不能证明 |
|---|---|---|---|
| apk_sha256 | 最终 APK 原始字节 | 签名与全部字节处理完成后 | 应用功能或安全门禁通过 |
| manifest_report_sha256 | 包名和版本提取报告 | 解码最终 APK 后 | 报告字段一定解释正确 |
| signing_report_sha256 | 签名方案与证书验证报告 | apksigner verify 后 | 私钥管理和操作授权正确 |
| provenance_digest | 构建来源声明文件 | 受控构建完成后 | 声明内容天然真实 |
| identity_digest | 规范化发布身份元组 | 所有必要字段齐备后 | 下游测试已经执行 |
| device_receipt_digest | 设备运行证据包 | 同一候选回归完成后 | 未测试设备同样通过 |
| store_receipt_ref | 目标轨道上传或交付回执 | 外部平台确认后 | 业务层授权策略正确 |
用 provenance 说明候选从哪里来
APK 摘要锁定字节,但不能解释这些字节由哪个构建者、什么参数和哪些材料生成。SLSA Provenance v1.1 将产物 subject 与构建者、构建类型、外部参数和依赖材料关联。发布身份可以把 apk_sha256 作为 provenance subject,并保存声明摘要与签名者,使审计能够从候选回到受控构建过程。
in-toto Attestation Statement v1 规定 subject 摘要与有类型 predicate 的通用绑定方式。身份系统不应把整份来源声明复制进公开页面,而应保存 provenance_ref、predicate_type、statement_digest 和验证状态。声明格式只解决结构;签名者是否可信、构建环境是否受控、参数是否符合发布策略仍由组织门禁决定。
来源证明不能取代 Manifest 和签名验证。构建声明声称 package_name 或证书正确,只能作为其记录的事实;验收仍要从实际 APK 提取包名、版本和证书并交叉核对。两边一致时形成更强证据,两边不一致时必须停止发布。没有实际 provenance 时应登记未接入,不能用 CI 任务成功截图冒充。
- 实际 apk_sha256 作为 provenance subject
- 保存构建者、构建类型、外部参数和材料引用
- 验证 statement 签名者与组织信任策略
- 声明中的包名、版本和证书与 APK 实测交叉核对
- CI 成功状态不代替来源证明
- 私有路径、密钥和客户数据不进入公开身份 JSON
- provenance 通过不扩大为运行时安全通过
用只读脚本生成规范化发布身份 JSON
下面的 Python 示例接收最终 APK、Manifest 提取报告和 apksigner 验证报告。它重算 APK 摘要,核对报告是否绑定该摘要,验证 package_name、versionCode、证书摘要和签名状态,再按固定字段生成 identity_digest。任何输入派生字段缺失或不一致都会以非零状态退出,输出只写到标准输出,不修改候选文件。
示例假定上游已把官方工具输出转换为受控 JSON,manifest-report 包含 package_name、version_code 和 version_name,signing-report 包含 verified、apk_sha256、certificate_sha256 与 schemes。生产流水线需要保留原始官方输出及其摘要,JSON 只是稳定接口。若存在多个签名者或证书轮换,应扩展 schema 并按批准策略规范化集合,不能直接套用单证书示例。
代码不会调用 apksigner,也不验证来源证明签名。它的职责是生成候选身份并阻止最常见的报告错配。发布完成还需要官方签名校验、更新基线比较、来源证明验证、静态门禁、设备回归和外部轨道回执。脚本输出中不包含私钥、口令、keystore 路径或内部主机,适合公开解释方法。
#!/usr/bin/env python3
import hashlib
import json
import re
import sys
from pathlib import Path
SHA256_PATTERN = re.compile(r"^[a-f0-9]{64}$")
PACKAGE_PATTERN = re.compile(r"^[A-Za-z][A-Za-z0-9_]*(?:[.][A-Za-z][A-Za-z0-9_]*)+$")
def read_json(path):
try:
value = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
print("cannot read report: " + str(exc), file=sys.stderr)
raise SystemExit(3)
if not isinstance(value, dict):
print("report must be an object", file=sys.stderr)
raise SystemExit(4)
return value
def file_sha256(path):
digest = hashlib.sha256()
with path.open("rb") as stream:
for block in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def require_digest(record, field):
value = record.get(field)
if not isinstance(value, str) or not SHA256_PATTERN.fullmatch(value):
print("invalid digest: " + field, file=sys.stderr)
raise SystemExit(5)
return value
def validate(apk_path, manifest, signing):
apk_digest = file_sha256(apk_path)
if signing.get("verified") is not True:
print("APK signature not verified", file=sys.stderr)
raise SystemExit(6)
reported_apk = require_digest(signing, "apk_sha256")
if apk_digest != reported_apk:
print("signing report APK mismatch", file=sys.stderr)
raise SystemExit(7)
package_name = manifest.get("package_name")
if not isinstance(package_name, str) or not PACKAGE_PATTERN.fullmatch(package_name):
print("invalid package_name", file=sys.stderr)
raise SystemExit(8)
version_code = manifest.get("version_code")
if not isinstance(version_code, int) or version_code < 1:
print("invalid version_code", file=sys.stderr)
raise SystemExit(9)
version_name = manifest.get("version_name")
if not isinstance(version_name, str) or not version_name.strip():
print("invalid version_name", file=sys.stderr)
raise SystemExit(10)
certificate = require_digest(signing, "certificate_sha256")
schemes = signing.get("schemes")
if not isinstance(schemes, list) or not schemes or not all(isinstance(x, str) for x in schemes):
print("invalid signing schemes", file=sys.stderr)
raise SystemExit(11)
identity = {
"schema_version": 1,
"package_name": package_name,
"version_code": version_code,
"version_name": version_name.strip(),
"certificate_sha256": certificate,
"apk_sha256": apk_digest,
"signing_schemes": sorted(set(schemes)),
}
encoded = json.dumps(identity, sort_keys=True, separators=(",", ":"), ensure_ascii=True)
identity["identity_digest"] = hashlib.sha256(encoded.encode("utf-8")).hexdigest()
return identity
def main():
if len(sys.argv) != 4:
print("Usage: release_identity.py APK MANIFEST_JSON SIGNING_JSON", file=sys.stderr)
raise SystemExit(2)
apk_path = Path(sys.argv[1])
manifest_path = Path(sys.argv[2])
signing_path = Path(sys.argv[3])
if not apk_path.is_file() or not manifest_path.is_file() or not signing_path.is_file():
print("input file missing", file=sys.stderr)
raise SystemExit(2)
identity = validate(apk_path, read_json(manifest_path), read_json(signing_path))
print(json.dumps(identity, sort_keys=True, indent=2, ensure_ascii=False))
if __name__ == "__main__":
main()用证据矩阵决定候选能否进入发布轨道
发布门禁应先冻结身份元组,再把所有证据挂到 identity_digest。Manifest 和签名报告提供包名、版本与证书事实,APK 摘要锁定字节,provenance 说明构建来源,更新基线证明与线上版本关系,设备回归证明指定环境下的运行结果,商店回执证明目标轨道接受了哪个候选。任何一项引用不同摘要都应阻止聚合。
负向用例应验证系统不会靠名称猜测。可以准备公开安全测试产物,分别交换 Manifest 报告、替换签名报告、重签相同版本、修改一个 ZIP 字节、重复 versionCode 或使用错误轨道基线,确认门禁在预期层拒绝。测试不需要真实生产私钥,报告只记录候选摘要、失败类别、修复责任和项目边界。
当前没有具体 APK、Manifest 报告、签名回执、线上基线和来源证明,因此本文只能给出身份模型、代码与验收方法,不能断言某个候选可直接更新、已经使用正确生产证书或完成发布。项目落地时应保存同一候选的真实回执;任何重构建、重签名、重打包或渠道处理都会生成新身份并触发全部门禁重跑。
| 证据项 | 绑定内容 | 合格条件 | 不能推出 |
|---|---|---|---|
| Manifest 报告 | package_name、versionCode 和显示版本 | 来自最终 APK 且报告摘要可追溯 | 组件和权限安全已通过 |
| 签名报告 | 验证状态、方案和证书摘要 | 证书命中批准集合且绑定同一 APK | 私钥管理和操作授权正确 |
| APK 摘要 | 最终签名字节 | 所有证据引用同一 SHA-256 | 应用功能与安全质量合格 |
| 更新基线 | 线上包名、版本和签名连续性 | 目标候选满足指定轨道条件 | 业务防重放策略完成 |
| 来源证明 | subject、builder、参数和材料 | 签名者与构建策略均通过 | 运行时不可篡改 |
| 设备回归 | 同一候选的安装、升级和核心流程 | 指定设备与路径结果可复核 | 未测试环境同样通过 |
| 轨道回执 | 上传或交付平台接受的候选 | 回执能关联元组或派生身份 | 最终用户已经全部更新 |
| 变化触发器 | 重构建、重签名、重打包和渠道处理 | 任何字节变化都重建元组并重验 | 旧回执可迁移到新文件 |
事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| Android Manifest 定义应用组件、权限、intent filter、SDK 约束和应用元数据,是最终候选身份与行为声明的重要输入。 | Android app manifest | 静态 Manifest 不能证明运行时访问控制、业务逻辑或所有组件行为安全。 |
| Android 发布需要区分应用签名密钥、上传密钥、证书以及 Play App Signing 场景下的责任。 | Sign your Android app | 发布文档不能确认某个实际 APK 已使用项目批准的生产证书,仍需对候选验证。 |
| apksigner 可以签名和验证 APK 的签名方案与证书,并要求签名后不再修改 APK。 | Android apksigner | 工具验证成功不证明 keystore 管理、签名操作授权、渠道处理或候选归档正确。 |
| Android 应用更新需要保持应用标识,并满足签名连续性和可接受 versionCode 等平台条件。 | How Android app updates work | 平台安装更新条件不等于服务端最低版本、撤销或其他业务防重放策略。 |
| in-toto Statement 使用产物摘要 subject 把具体产物与有类型的声明负载绑定。 | in-toto Attestation Statement v1 | 声明格式不保证内容真实,仍需验证签名者、签名和组织策略。 |
| SLSA provenance 可以将产物主体与构建者、构建类型、外部参数和依赖材料关联。 | SLSA Provenance v1.1 | provenance 只能证明记录的构建过程,不能单独证明运行时安全或发布质量。 |
| 工程判断:package_name、versionCode、证书摘要、APK 摘要和来源证明应组成不可拆分的候选身份元组。 | 工程判断:基于 Android 更新身份、签名连续性与证据防错配原则 | 字段扩展、证书集合和轨道基线需要按项目发布模型设计,示例不是平台标准。 |
| 当前没有具体 APK、Manifest 报告、签名回执、线上基线与来源证明,不能断言任何候选已满足发布条件。 | 项目证据尚未接入 | 文章只提供公开安全的身份模型、代码和证据矩阵,不包含客户案例、排名、性能或实测通过结论。 |
工程常见问题
为什么 APK 文件名和 versionName 不能作为发布身份?
两者都可以重复或被人工修改,无法锁定应用更新命名空间、版本顺序、签名身份和真实字节。发布身份至少需要最终 package_name、versionCode、证书摘要和 APK SHA-256;versionName 只作为面向用户的辅助显示字段。
package_name、versionCode 和证书都相同,是否就是同一个 APK?
不一定。同一配置可以重新构建出字节不同的 APK,资源压缩、依赖、时间戳或工具链变化都可能改变产物。必须比较最终 APK 的 SHA-256 摘要。摘要不同就属于新候选,原有静态与设备回执不能自动迁移。
apksigner 验证通过是否说明使用了正确生产证书?
不说明。它证明候选的签名结构和证书在工具验证范围内有效,证书是否属于产品和目标轨道仍要与批准的证书摘要集合比较。密钥托管、签名权限和上传流程也需要独立审计。
Play App Signing 场景为什么仍要记录本地候选证书?
上传密钥与应用签名密钥责任不同,本地上传候选和用户最终获得的产物可能具有不同证书语义。身份记录应明确候选所处阶段,并把上传回执或派生产物身份另行关联,不能用一个模糊的“生产证书”字段覆盖。
provenance 已绑定 APK 摘要,为什么还要重新解析 Manifest 和签名?
来源证明陈述的是构建系统记录的事实,不能替代对实际产物的观察。验收要从 APK 提取包名、版本和证书,再与 provenance 交叉核对。两者一致增强可信度,不一致则说明候选或报告发生错配。
怎样防止测试报告被错误复用到重签名或渠道包?
所有报告必须引用规范化 identity_digest 和 apk_sha256。渠道处理或重签名后重新计算摘要、提取 Manifest、验证证书并生成新元组。聚合系统只接受精确元组匹配,文件名、版本名或人工备注相同都不能复用旧结果。