先看结论与判断条件

  • 包名回答更新归属哪个已安装应用,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 字节,流水线应判定为不同候选冲突,而不是覆盖旧文件。

Manifest 身份字段的提取与误区
观察项可靠来源常见误区门禁规则
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 路径或内部主机,适合公开解释方法。

从 Manifest 与签名报告生成 APK 发布身份
#!/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 报告、签名回执、线上基线和来源证明,因此本文只能给出身份模型、代码与验收方法,不能断言某个候选可直接更新、已经使用正确生产证书或完成发布。项目落地时应保存同一候选的真实回执;任何重构建、重签名、重打包或渠道处理都会生成新身份并触发全部门禁重跑。

APK 发布身份的最小证据矩阵
证据项绑定内容合格条件不能推出
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.1provenance 只能证明记录的构建过程,不能单独证明运行时安全或发布质量。
工程判断: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、验证证书并生成新元组。聚合系统只接受精确元组匹配,文件名、版本名或人工备注相同都不能复用旧结果。

想用自己的 App 验证?

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

继续阅读: APK、DEX 与签名完整性如何一起验收