先看结论与判断条件

  • 轨道 API 返回的 versionCodes、status、userFraction 和 targeting 是控制面事实,不等于设备已经收到候选。
  • 候选身份元组至少包含 applicationId、versionCode、签名证书摘要、产物摘要、构建变体和 provenance 摘要。
  • versionCode 只能连接轨道记录与上传版本,不能替代本地文件摘要、证书身份和构建来源。
  • inProgress、halted、completed 与 draft 必须原样保存;halted 不能写成已安装版本回滚或召回。
  • 发布操作回执与后续查询回执应分开保存,使 API 接受、编辑提交、审核与可见性保持不同证据状态。
  • 任何重新构建、重新签名、轨道切换或用户比例变化都会使旧绑定失效,必须生成新的不可变回执。

先拆开平台控制面与本地候选身份

发布团队常见的证据错配,是把 Track update 返回成功直接写成“这个 APK 已上线”。Google Play Developer API track update 的返回对象描述编辑中的轨道配置,它能够确认服务端接受了受授权请求并返回 Track,但响应本身没有本地 APK 文件路径、文件摘要或签名证书摘要。若流水线只保存 HTTP 成功和 track 名称,就无法证明审核时看到的本地候选与轨道中 versionCode 对应的上传对象是同一个文件。

正确做法是先为候选生成不可变身份元组,再把轨道事实附加到该元组。身份元组至少记录 applicationId、versionCode、签名证书 SHA-256、APK 或 AAB 主体 SHA-256、构建变体、构建任务和 provenance 摘要。轨道回执记录 edit、track、release status、versionCodes、userFraction、countryTargeting、更新优先级与查询时间。两部分各自保留原始来源,不把缺失字段凭空补入平台响应。

绑定后的结论仍然分层。若本地摘要与构建证明一致,只能确认候选来源;若 versionCode 出现在目标 track,只能确认轨道引用该版本;若后续查询仍显示期望状态,只能确认控制面状态。设备是否可见、商店审核是否结束、分发是否覆盖目标地区以及业务质量是否合格,需要额外证据。报告应保留这些边界,避免一个绿色状态遮蔽尚未完成的环节。

候选与轨道绑定的两类事实
事实域核心字段可信来源不能证明
本地产物主体摘要、包名、版本码只读产物检查轨道已接受
签名身份证书摘要与轮换证明签名校验工具与发布记录设备已更新
构建来源builder、buildType、材料摘要provenance运行时质量
轨道配置track、status、versionCodesTrack API 回执本地文件摘要
分阶段范围userFraction、countryTargetingTrack release 对象每台设备已收到
后续状态查询时间与当前 release独立查询回执业务功能通过

轨道回执中哪些字段必须原样保留

Google Play Developer API tracks 描述 Track 及其 releases。每个 release 可以记录 name、versionCodes、releaseNotes、status、userFraction、countryTargeting 和 inAppUpdatePriority。门禁不应只截取 versionCode,因为同一版本处于 draft、inProgress、halted 或 completed 时含义不同;灰度比例和地区目标也会改变实际覆盖范围。保存结构化原始响应,才能在之后解释当时的发布意图。

status 必须按 API 值保存,不要翻译成一个模糊的“成功”。draft 表示尚未面向用户发布,inProgress 表示分阶段推进,halted 表示停止继续扩大该发布,completed 表示发布配置完成。halted 并不会自动移除已经安装该版本的用户,因此它不能作为客户端回滚、撤回或数据恢复回执。若业务要求回退,需要另外定义可交付版本、服务端兼容和用户状态处置。

userFraction 只有与 status、track、versionCodes 和查询时间一起保存才有意义。单独看到一个比例,无法知道它对应哪个版本、哪个国家范围或哪个编辑状态。countryTargeting 也不能被忽略,因为相同轨道名在不同目标范围下的用户可见性不同。回执应保持字段缺失与显式值的区别,API 未返回的值不能由流水线默认补为全量。

Track release 字段的审计含义
字段用于回答必须绑定常见误写
track发布进入哪个轨道package 与查询时间把轨道名当成候选身份
versionCodes该 release 引用了哪些版本本地 versionCode 与上传记录把版本码当作文件摘要
status发布控制面处于何种状态同一 release 快照所有状态都写成已上线
userFraction分阶段覆盖目标inProgress 或 halted 语义写成实际安装比例
countryTargeting目标国家范围轨道与版本集合省略后宣称全地区可见
updatePriority应用内更新优先级参数对应 versionCodes写成系统强制更新

versionCode 是连接键,不是产物指纹

versionCode 是 Play 轨道记录与 Android 更新判断的重要连接键,但它不是加密摘要。同一个本地构建环境可以生成内容不同却使用相同 versionCode 的未上传文件,团队也可能在上传后错误地拿另一份同版本产物做测试。门禁必须把 versionCode 与 artifactSha256 同时记录,随后通过上传回执或受控构建流程证明轨道引用的版本来自该候选。仅靠数值相同不能完成文件级身份确认。

Android publish your app 指南将 release 变体、签名产物、测试和依赖服务准备视为发布前工作,上传只是流程中的一个环节。候选绑定因此应在上传前完成:冻结产物摘要、证书摘要和测试回执;上传时记录版本与操作身份;更新轨道时记录 Track 响应;提交编辑后再查询当前轨道。任何阶段重新构建都会生成新主体,不能继续沿用之前的文件级门禁。

当一个 release 的 versionCodes 包含多个版本时,回执应保存完整集合,不要只保留目标版本。目标版本存在说明它被该 release 引用,其他版本则可能仍参与覆盖或兼容策略。若同一 versionCode 出现在多个 release 或多个轨道,发布负责人必须根据期望轨道、状态和范围做明确选择。脚本可以阻断歧义,但最终发布意图仍需责任人确认。

应用更新身份还要检查包名与签名连续性

How Android app updates work 说明,Android 将更新与 applicationId、签名身份和 versionCode 条件关联。候选绑定不能只检查轨道里的版本码,还要在本地检查包名与发布应用一致,并核对签名证书或合法轮换证明。包名错配意味着它不是同一应用更新,证书错配则可能无法形成有效更新路径。平台条件成立仍不代表业务迁移和数据兼容已经通过。

签名证书摘要应来自候选产物的只读验证,而不是构建脚本中的配置字符串。若使用 Play App Signing,需要区分上传密钥与应用签名密钥在流程中的角色,并在内部回执中写清证书摘要的来源。Track API 不返回本地签名证书摘要,因此不能声称平台响应已经核对该字段;这项关系要由上传记录、Play 配置和组织控制补足。

versionCode 必须满足平台更新顺序,但更高的数值不等于更安全或更兼容。数据库迁移、文件格式、服务端协议和加固配置都可能让新候选无法安全替换旧版本。本文只处理轨道回执与候选身份绑定,不给出业务防重放或版本回滚策略。发布报告应把平台更新条件和应用自己的兼容门禁分开,分别保存证据。

候选身份元组的核对规则
字段来源核对方法错配处理
applicationId最终 Manifest与 Play 应用包名精确比较阻断并更换候选
versionCode最终 Manifest在目标 release versionCodes 中查找阻断或重建轨道编辑
签名证书摘要签名验证输出与批准的发布身份或轮换证明比较阻断并调查签名链
产物摘要最终 APK 或 AAB与上传和 provenance 主体比较旧回执失效
构建变体构建记录与发布配置和测试范围比较补齐正确变体证据
provenance 摘要不可变构建证明验证主体和材料绑定不得宣称来源已确认

provenance 连接产物摘要与构建来源

SLSA Provenance v1.1 定义的 provenance 可描述 subject、builder、buildType、外部参数和依赖 materials。对轨道绑定而言,最关键的是候选 artifactSha256 必须出现在 provenance subject 中,并且构建者与构建类型属于批准范围。这样可以证明发布门禁检查的文件来自记录的构建过程,而不是在上传前被未知步骤替换。

签名证书摘要不是 SLSA provenance 必须提供的标准发布字段。组织可以在受控的外部参数或单独签名回执中记录证书摘要,再由绑定程序比较,但必须标明这是项目约束而非规范保证。若 provenance 只覆盖未签名 AAB,而轨道绑定针对最终 APK,则还需记录从 AAB 到分发 APK 的受控转换关系,不能用一个不同主体的摘要直接替代。

provenance 同样有边界。它证明声明的构建来源,不证明 Track API 当前状态,不证明签名密钥未被滥用,也不证明候选运行正确。门禁需要同时持有本地产物验证、provenance 验证、轨道更新响应和后续轨道查询。缺少任一类时,应输出具体的未确认状态,不把可追溯性扩张为上线或安全结论。

不可变发布回执的组成
回执绑定输入确认范围失效条件
候选身份回执包名、版本、证书、主体摘要本地被测对象文件或签名变化
构建来源回执subject、builder、buildType、materials声明的生成关系主体或证明变化
轨道更新回执edit、track、release 请求与响应平台接受控制请求新编辑或配置变化
编辑提交回执edit 与提交时间编辑进入后续处理提交失败或编辑废弃
轨道查询回执track 当前快照与查询时间控制面当前状态后续轨道变化
运行验证回执设备、安装来源、候选身份指定环境中的行为候选或环境变化

发布状态要按时间形成证据链

一次发布至少有三个时间点:本地候选冻结、轨道更新返回、编辑提交后的重新查询。轨道更新返回只能记录操作已被 API 接受,编辑提交回执说明该编辑进入平台后续处理,重新查询才能确认目标 track 当前展示的 release 状态。商店审核和设备可见性仍可能在另一时间发生,因此报告应保存 observedAt,而不是把不同时间的事实拼成一个同步状态。

查询还需要防止读错应用与轨道。请求中的 packageName、editId 和 track 要进入回执摘要,响应中的 track 必须与期望值精确匹配。若目标 versionCode 不存在、出现在多个 release、状态与期望不符或用户比例缺失,脚本应失败关闭。对于 halted 状态,报告应明确它停止继续推进,不声称已经安装的客户端被回滚。

分阶段发布期间,userFraction 或 countryTargeting 的任何变化都应产生新回执。旧回执可以保留审计历史,但不能继续代表当前范围。团队若要扩大比例,应先关联当前候选的质量信号和审批,再执行新的轨道更新。自动化可以校验字段和身份,不能替发布责任人判断异常率、服务端承载和业务风险是否允许扩大。

轨道状态的可接受表述
观察可以确认仍待确认禁止表述
update 返回 Track平台接受轨道更新请求编辑提交和后续状态候选已面向用户上线
查询为 draft当前配置仍为草稿后续提交决定正在灰度
查询为 inProgress控制面处于分阶段发布实际设备覆盖与质量所有目标用户已更新
查询为 halted停止继续扩大该 release已安装用户和恢复方案客户端已自动回滚
查询为 completed轨道配置完成发布地区可见性与设备行为所有设备安装成功
目标版本缺失当前回执未找到绑定是否读错编辑或轨道沿用旧成功回执

用校验脚本拒绝版本、轨道与来源错配

下面的 Python 脚本读取候选身份 JSON、Track 查询 JSON 和 SLSA provenance JSON。候选文件提供 packageName、versionCode、artifactSha256、signingCertSha256、expectedTrack、expectedStatus 与批准的 builder;Track 文件保留 packageName、track 和 releases;provenance 提供 subject 与 predicate.builder。脚本只比较结构化字段,不调用 Play API,也不接触服务账号凭据。

脚本要求目标 versionCode 在目标 track 中恰好出现一次,release 状态与期望一致,候选摘要在 provenance subject 中恰好匹配,并核对 builder 与摘要格式。它输出 track、versionCode、status、userFraction、证书摘要和 provenance 是否匹配,但不声称证书摘要来自 Track API。任何字段缺失、类型错误、重复版本或来源错配都会返回非零状态,适合接入发布门禁。

输入 JSON 的可信来源仍需组织控制。candidate.json 应由只读产物检查生成,track.json 应来自受授权的独立查询,provenance 应经过签名或可信分发验证。若有人可以同时修改三份输入,字段一致不代表真实。脚本也不验证商店审核和设备可见性,因此通过后的结论只能写成“本地候选、构建来源与当前轨道字段一致”。

校验 Play Track、候选身份与 provenance 主体
from pathlib import Path
import json
import re
import sys

if len(sys.argv) != 4:
    raise SystemExit(2)
paths = [Path(value) for value in sys.argv[1:]]
if any(not path.is_file() for path in paths):
    raise SystemExit(2)
try:
    candidate, track, provenance = [json.loads(path.read_text(encoding="utf-8")) for path in paths]
except (OSError, UnicodeError, json.JSONDecodeError):
    raise SystemExit(2)
required = ("packageName", "versionCode", "artifactSha256", "signingCertSha256", "expectedTrack", "expectedStatus", "expectedBuilder")
if any(key not in candidate for key in required):
    raise SystemExit(2)
if not isinstance(candidate["versionCode"], int) or candidate["versionCode"] <= 0:
    raise SystemExit(2)
sha_pattern = re.compile(r"^[a-f0-9]{64}$")
if not sha_pattern.fullmatch(candidate["artifactSha256"]) or not sha_pattern.fullmatch(candidate["signingCertSha256"]):
    raise SystemExit(2)
if track.get("packageName") != candidate["packageName"] or track.get("track") != candidate["expectedTrack"]:
    raise SystemExit(3)
releases = track.get("releases")
if not isinstance(releases, list):
    raise SystemExit(2)
target = str(candidate["versionCode"])
matches = []
for release in releases:
    version_codes = release.get("versionCodes", [])
    if not isinstance(version_codes, list) or any(not isinstance(value, str) for value in version_codes):
        raise SystemExit(2)
    if target in version_codes:
        matches.append(release)
if len(matches) != 1:
    raise SystemExit(3)
release = matches[0]
if release.get("status") != candidate["expectedStatus"]:
    raise SystemExit(3)
subjects = provenance.get("subject")
if not isinstance(subjects, list):
    raise SystemExit(2)
subject_matches = [item for item in subjects if item.get("digest", {}).get("sha256") == candidate["artifactSha256"]]
if len(subject_matches) != 1:
    raise SystemExit(3)
predicate = provenance.get("predicate", {})
builder_id = predicate.get("runDetails", {}).get("builder", {}).get("id")
if builder_id != candidate["expectedBuilder"]:
    raise SystemExit(3)
report = {
    "packageName": candidate["packageName"],
    "track": track["track"],
    "versionCode": candidate["versionCode"],
    "status": release["status"],
    "userFraction": release.get("userFraction"),
    "artifactSha256": candidate["artifactSha256"],
    "signingCertSha256": candidate["signingCertSha256"],
    "provenanceSubjectMatched": True,
    "builderMatched": True,
    "distributionConfirmed": False
}
print(json.dumps(report, ensure_ascii=False, indent=2))

把门禁结果写成可审计发布决定

合格回执应包含四组材料:候选身份、构建来源、轨道操作和当前查询。候选身份保存包名、版本码、证书与主体摘要;构建来源保存 provenance 摘要及验证结果;轨道操作保存请求、响应和 edit;当前查询保存 track、release 字段与观察时间。发布决定引用这些回执摘要,不复制可变的控制台截图,也不把服务账号令牌写入报告。

NIST SP 800-218 SSDF 强调把安全活动、变更和供应链风险处理纳入开发流程。对应到轨道绑定,就是让候选冻结、测试、上传、审批、发布和查询形成有责任人的证据链。若任何摘要错配或字段缺失,发布流程应停止并定位输入来源。框架不定义 Play API 的具体状态,也不证明当前候选已达到安全、兼容或业务目标。

门禁通过后的决定必须带有限定词。例如“候选摘要与 provenance subject 一致,目标 versionCode 出现在 production 轨道的 inProgress release 中,观察时间为某时刻”;不要缩写成“生产已上线”。若仍待商店审核、地区可见性或设备验证,应逐项列出。这样下一个操作者能直接看到缺口,而不是重新猜测成功截图代表什么。

准备材料与适用限制

实施前应准备最终 APK 或 AAB、产物摘要、最终 Manifest 身份、签名证书摘要、构建变体、provenance、测试回执、目标 packageName、track、期望 status 和分阶段策略。受授权的 Play 操作应由最小权限服务身份执行,原始响应与请求摘要进入受控存储。文章中的代码不处理认证,也不建议把访问令牌写进命令行、日志或内容仓库。

本方法不解决所有发布风险。它不证明 Play 审核通过,不证明每个设备收到相同 APK,不验证 split 生成、运行时兼容、业务数据迁移或服务端防重放,也不替代签名轮换验证。Track 字段可能在操作后继续变化,所以观察时间和重新查询是回执的一部分。没有真实平台响应和同候选产物时,只能保留未接入或待确认状态。

准备接入评估时,可在御盾中央平台提交候选摘要、签名证书摘要、构建证明、目标轨道、发布请求与脱敏后的 Track 响应。登录、注册、申请、价格和控制台操作统一由中央平台承接。具体是否可发布仍取决于项目审批、Play 控制面状态和同候选验证,不因一份静态字段校验报告自动成立。

事实依据与适用边界

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

本文判断事实或工程依据适用限制
Google Play Track release 可以记录 versionCodes、status、userFraction、countryTargeting 与更新优先级。Google Play Developer API tracks 描述 Track 与 release 的字段和状态。这些字段是控制面事实,不包含本地 APK 文件摘要,也不证明设备已经完成安装。
受授权客户端可以提交轨道更新,并从 API 获得返回的 Track 对象。Google Play Developer API track update 描述 edits.tracks.update 请求与响应。更新请求成功不证明编辑已经完成后续处理、商店审核通过或业务质量合格。
Android 发布前需要准备 release 变体、签名产物、测试和应用依赖服务。Android publish your app 描述发布准备、构建与签名等流程环节。通用发布指南不证明某个具体候选能够直接上架,也不替代商店政策审核。
Android 应用更新依赖 applicationId、兼容签名身份与可接受的 versionCode。How Android app updates work 描述应用更新的包身份、签名和版本条件。平台更新条件不证明数据库迁移、业务协议、加固配置和运行时兼容已经通过。
provenance 可以把产物主体与构建者、构建类型、外部参数及依赖材料关联。SLSA Provenance v1.1 定义 subject、builder、buildType、parameters 和 materials 等关系。来源证明不能单独确认 Track 当前状态、签名密钥使用正确或候选运行安全。
安全发布应保留构建、验证、变更和供应链风险处理证据。NIST SP 800-218 SSDF 给出将安全实践纳入软件开发与发布生命周期的组织级框架。SSDF 不定义 Play 轨道字段,也不证明任何候选或发布已经完成。
versionCode 应作为 Track 与候选的连接键,同时保留产物和证书摘要。工程判断:版本码可重复出现在不同本地文件中,摘要和签名身份才能限制证据被错配。本地元组一致仍需可信上传记录,才能进一步确认平台所持对象的来源。
轨道更新响应和提交后的独立查询应作为两个时间点的回执保存。工程判断:控制请求被接受与当前轨道状态是不同事实,配置也可能在之后继续变化。查询结果仍不能替代商店审核、用户可见性和真实设备验证。

工程常见问题

Track update 返回成功是否说明 APK 已经上线?

不能。它说明平台接受了受授权的轨道更新请求并返回 Track 对象。还需提交编辑、重新查询状态,并分别核对商店审核、地区可见性和设备行为。

versionCode 与轨道记录一致,为什么还要保存 APK 摘要?

versionCode 不是文件指纹,本地可能存在内容不同但版本码相同的文件。产物摘要、签名证书摘要和构建证明共同限制回执被挪用到错误候选。

halted 是否表示已经安装的版本被自动回滚?

不是。halted 表示停止继续推进分阶段发布,不影响已经安装该版本的用户。客户端回退、召回与数据恢复需要独立方案和证据。

completed 是否等于所有设备都安装了新版本?

不等于。completed 描述轨道 release 的控制面状态。设备资格、地区、缓存、用户更新选择和运行结果仍需其他数据确认。

provenance 匹配后还需要签名检查吗?

需要。provenance 绑定声明的构建主体和来源,但签名身份、上传过程和 Play 配置属于其他控制。证书摘要应来自候选验证并与批准身份比较。

申请发布候选绑定评估要准备哪些材料?

准备最终 APK 或 AAB、主体摘要、包名、versionCode、签名证书摘要、构建变体、provenance、测试回执、目标轨道、发布请求和脱敏 Track 响应,再通过御盾中央平台提交。

想用自己的 App 验证?

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

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