先看结论与判断条件
- 当前版本、目标版本、渠道、安装方式和远端策略是不同输入,不能压成一个客户端 updateAvailable 布尔值。
- versionCode 参与 Android 更新判断,versionName 主要用于展示,两者都不能单独证明目标工件可信。
- VMP 优先保护渠道映射、远端状态到本地提示的有限转换和关键失败分支,不整体包住下载与安装框架。
- 强制更新提示只能约束官方客户端体验,服务端仍要用最低允许版本、接口兼容和账户策略决定是否继续服务。
- 发布元数据需要签名、版本、过期与一致快照等控制,以识别回滚、冻结和混搭风险。
- 最终验收必须把远端策略、APK 摘要、签名证书、PackageInstaller 会话和设备结果绑定到同一候选。
先定边界:客户端负责展示与编排,发布端拥有资格裁决
应用内更新流程通常从本地版本读取开始,经过渠道识别、远端元数据获取、资格比较、弹窗展示、包下载和安装会话。真正值得进入 VMP 的不是整条链,而是容易被修改后改变客户端行为、同时又具有有限输入输出的策略映射,例如渠道到更新源的选择、响应状态到提示类型的转换和失败时禁止降级。
远端发布系统必须决定哪些版本仍被允许、哪个渠道可见哪个候选、是否撤回故障版本、灰度比例和最低可服务版本。客户端中的 currentVersion、isForced 或 updateUrl 都可能被篡改,只能作为待验证输入。服务器与分发平台不能因为官方 App 上隐藏了按钮,就假设旧客户端无法继续调用接口。
Android 平台还会核对 applicationId、versionCode 和签名连续性。即便客户端本地判断 eligible=true,目标 APK 证书不兼容、版本码不满足条件、split 集合不一致或安装会话失败,更新仍不会成立。因此文章把本地展示、远端资格和平台安装分为三层,不把任何一层结果外推成整体成功。
| 层 | 主要输入 | 可以决定 | 不能单独决定 |
|---|---|---|---|
| 客户端展示 | 当前版本与远端状态 | 按钮、文案、提醒时机 | 目标包可信 |
| 发布端策略 | 渠道、灰度、撤回、最低版本 | 是否提供候选 | 设备安装成功 |
| 平台安装 | 包名、版本、证书、splits | 安装或拒绝 | 业务兼容完成 |
| 服务端 API | 客户端版本与账户状态 | 是否继续服务 | APK 签名正确 |
| 候选归档 | APK 摘要与签名回执 | 工件身份 | 用户已安装 |
| VMP 保护 | 方法与规则范围 | 增加修改实现成本 | 最终授权与发布控制 |
版本比较先用平台语义,展示字符串不能承担安全判断
Android 更新条件包含 applicationId、兼容签名或轮换证明以及可接受的 versionCode。versionName 是用户可读字符串,可能包含渠道后缀、语义化版本或营销名称,不能用普通字符串大小决定是否更新。客户端应读取安装包的实际 versionCode,并把比较规则和远端候选版本作为结构化字段。
即便 versionCode 更高,也不等于候选可安装或应立即推送。发布端可能撤回更高版本、限制某个渠道、暂停某个架构或要求先经过中间版本。客户端只做明显的格式与方向预检,最终 eligible 状态由带版本与过期控制的远端元数据给出,安装仍由平台核对签名和包身份。
版本比较方法若进入 VMP,应先消除隐式类型转换、溢出、空值和降级容错。输入仅包含当前 versionCode、目标 versionCode、渠道和受验证策略状态,输出是展示枚举而非授权布尔。未知策略、时间过期或渠道不匹配都进入 fail-closed 状态,不得自动退回任意下载 URL。
| 字段 | 用途 | 错误用法 | 门禁 |
|---|---|---|---|
| applicationId | 确认应用身份 | 按相似名称匹配 | 精确一致 |
| versionCode | 平台版本顺序 | 等同候选可信 | 整数范围与策略 |
| versionName | 界面展示 | 字符串比较大小 | 不参与资格裁决 |
| channel | 选择发布策略 | 客户端任意切换 | 远端允许集合 |
| artifactDigest | 绑定准确工件 | 只保存文件名 | 下载后重算 |
| policyVersion | 识别规则快照 | 默认为最新 | 签名与单调检查 |
渠道与灰度选择必须由远端签名策略约束
渠道选择常被实现为客户端常量或构建变量,VMP 可以提高直接改写渠道映射的成本,但发布端仍要把授权渠道、区域、设备条件和候选摘要写入受信元数据。客户端传来的 channel 只是筛选线索,不能让请求者自由选择内部测试或高权限分发源。
灰度资格也不应仅依赖本地随机数或可预测设备标识。发布端可基于稳定主体和策略版本计算分桶,并返回有限期结果;客户端只负责按结果展示。若攻击者修改本地 bucket,下载端和更新 API 仍要验证该主体是否有资格获取候选,避免隐藏链接成为唯一边界。
撤回比推送更需要远端控制。某个版本发现故障后,发布端应停止返回工件、缩短元数据有效期并记录撤回原因。客户端不能把上次缓存的 eligible 结果无限使用。缓存应绑定策略版本、到期时间、渠道和候选摘要,过期或不一致时只显示重新检查,不继续安装。
强制更新提示不是强制安全控制
所谓强制更新通常指官方客户端阻止进入主界面并要求升级。这种分支可以进入 VMP,避免简单修改一个布尔值就跳过提示,但它只能约束该客户端实现。攻击者可修改流程、调用旧接口或使用自动化请求,因此真正的最低版本策略必须在服务端接口上独立执行。
服务端拒绝旧版本也要有边界。版本头或 User-Agent 都来自客户端,不能单独作为高风险授权依据;更可靠的是在认证会话、协议能力和资源操作上执行兼容策略,并提供可审计的错误码。涉及紧急漏洞时,还要撤销相关 token、密钥或发布工件,而不是只弹出不可关闭对话框。
用户体验需要区分推荐更新、计划停服、当前版本已撤回和安装暂不可用。VMP 中只保留状态到界面动作的确定性映射,文案、无障碍和平台安装 UI 保持可迭代。网络失败、策略过期或下载异常不得被解释成继续使用已撤回版本的授权。
| 状态 | 客户端展示 | 服务端动作 | 禁止降级 |
|---|---|---|---|
| recommended | 可稍后提醒 | 维持兼容接口 | 不伪装强制 |
| required-soon | 明确截止时间 | 记录最低版本计划 | 过期缓存不沿用 |
| blocked | 仅保留更新入口 | 拒绝不兼容操作 | 本地跳过不放行 |
| withdrawn | 停止安装候选 | 移除下载与令牌 | 不使用缓存 URL |
| policy-expired | 要求重新检查 | 不返回旧资格 | 不默认 eligible |
| installer-unavailable | 提供安全重试 | 保持候选状态 | 不改用未知来源 |
更新元数据要抵抗回滚、冻结和混搭
The Update Framework specification 把签名、版本、过期时间和一致快照作为更新系统的重要控制,用于识别回滚、冻结和混搭风险。移动项目不一定完整采用 TUF,但仍应借鉴这些约束:元数据由受控角色签名、版本单调增长、有限期有效,并把目标文件长度与摘要绑定。
回滚风险是客户端接受旧策略或旧候选,冻结风险是攻击者长期阻止看到新元数据,混搭风险是不同快照中的目标和元数据被拼接。单纯 HTTPS 不能替代离线可验证元数据,因为 CDN、缓存、代理和运维错误也可能返回陈旧对象。VMP 保护解析器不能修复服务端未签名或无过期的策略。
客户端持久化最近接受的策略版本和到期时间时,记录可被本地修改,因而只能作为拒绝明显回滚的辅助信号。发布端仍要维持当前状态、撤回列表和下载授权。发现版本倒退、签名错误或目标摘要不符时,客户端清除临时下载并上报,不得回退到内置不受控 URL。
签名连续性与 PackageInstaller 会话属于平台门禁
Android 发布需要区分应用签名密钥、上传密钥、证书与 Play App Signing 责任。应用内更新下载到的 APK 必须与已安装应用保持兼容签名或有效轮换关系。客户端本地策略无法改变 Package Manager 的身份判断,也不应携带私钥或自己重签候选。
split 安装会话要求 base、packageName、versionCode 与签名证书等保持一致。若更新由多个 APK 组成,资格响应应绑定完整工件集合,每个文件有长度和摘要;客户端创建会话前复算,PackageInstaller 负责最终安装约束。只验证 base 或只比较版本号不足以证明会话完整。
安装成功也不等于业务验收完成。发布门禁还要记录升级前历史包、安装后版本、首次启动、数据迁移和关键流程。若更新失败,区分策略资格、下载完整性、签名身份、split 集合、存储空间和业务崩溃,不能把所有错误归因于 VMP 或强制提示。
VMP 保护有限策略映射,不包住下载器和安装器
优先进入 VMP 的方法包括渠道白名单映射、受验证元数据状态到 UI 枚举的转换、撤回状态的失败关闭、缓存策略版本比较和安装前关键条件组合。这些方法应是纯函数或小型编排器,输入显式,输出可枚举,便于固定测试向量和差分回归。
HTTP 下载、证书验证、JSON 或 CBOR 库、文件 I/O、PackageInstaller 调用和平台 Intent 通常保留在外。整体虚拟化会增加系统耦合和性能回归,却不提高远端元数据真实性。成熟库通过依赖锁定、供应链审查和标准向量治理,VMP 只保护业务特有的组合决策。
保护边界还要检查旁路:旧入口是否能直接安装 URL、调试菜单是否切换内部渠道、安装失败是否回落到浏览器未知来源、策略解析错误是否默认为可更新。任何旁路都从业务代码修复,再更新保护规则;不能用扩大类级范围掩盖不清晰的数据流。
| 代码单元 | 默认建议 | 主要价值 | 验证方式 |
|---|---|---|---|
| 渠道白名单 | 优先纳入 | 增加改写来源成本 | 未知渠道拒绝 |
| 策略状态映射 | 优先纳入 | 保护强制与撤回分支 | 枚举全覆盖 |
| 缓存版本比较 | 按需纳入 | 拒绝明显回滚 | 版本倒退用例 |
| 元数据签名库 | 通常不纳入 | 标准密码学实现 | 官方向量 |
| 下载与文件 I/O | 不整体纳入 | 平台和性能耦合 | 摘要与故障注入 |
| PackageInstaller | 不纳入 | 系统安装职责 | 真实会话回执 |
用公开样例输出本地展示与远端确认边界
下面的 Python 示例读取不含真实地址和凭据的 JSON 文件,要求包含当前版本、目标版本、渠道、策略状态、策略版本和到期时间。它验证字段类型与允许枚举,输出本地可展示的提示,同时列出必须由发布端、签名工具和 PackageInstaller 确认的项目。
脚本故意不下载、不安装、不验证生产签名,也不把 eligible 写成授权结论。它只说明给定公开样例在教学规则下应显示什么;目标 APK 摘要、签名证书、撤回状态和 split 集合仍由真实发布证据决定。输入字段缺失、渠道未知、版本倒退或策略过期时非零退出。
正式流水线可以把该边界表作为单元测试投影,再增加签名元数据验证、候选摘要重算和设备会话。VMP 前后应运行同一输入,输出发生变化就阻止发布。通过不能外推为远端策略可信、APK 可安装或所有设备升级成功。
import json
import sys
import time
from pathlib import Path
+
+if len(sys.argv) != 2:
+ raise SystemExit("usage: inspect_update_policy.py public-policy.json")
+policy_path = Path(sys.argv[1]).resolve()
+if not policy_path.is_file():
+ raise SystemExit(f"policy file missing: {policy_path.name}")
+with policy_path.open("r", encoding="utf-8") as stream:
+ policy = json.load(stream)
+if not isinstance(policy, dict):
+ raise SystemExit("policy root must be a JSON object")
+
+required = {"current_version_code", "target_version_code", "channel", "status", "policy_version", "expires_at"}
+missing = sorted(required - set(policy))
+if missing:
+ raise SystemExit(f"missing fields: {missing}")
+for name in ("current_version_code", "target_version_code", "policy_version", "expires_at"):
+ if not isinstance(policy[name], int) or isinstance(policy[name], bool):
+ raise SystemExit(f"{name} must be an integer")
+allowed_channels = {"public", "enterprise", "managed-test"}
+allowed_status = {"recommended", "required-soon", "blocked", "withdrawn"}
+if policy["channel"] not in allowed_channels:
+ raise SystemExit("unknown update channel")
+if policy["status"] not in allowed_status:
+ raise SystemExit("unknown policy status")
+if policy["target_version_code"] < policy["current_version_code"]:
+ raise SystemExit("target version is lower than the installed version")
+if policy["expires_at"] <= int(time.time()):
+ raise SystemExit("policy snapshot has expired")
+
+local_display = {
+ "show_update": policy["target_version_code"] > policy["current_version_code"],
+ "prompt_kind": policy["status"],
+ "channel_label": policy["channel"],
+}
+remote_required = [
+ "metadata signature and monotonic policy version",
+ "channel and rollout eligibility",
+ "artifact length and digest",
+ "withdrawal and minimum-served-version state",
+ "APK signing identity and PackageInstaller result",
+]
+print(json.dumps({
+ "local_display_only": local_display,
+ "must_confirm_remotely_or_by_platform": remote_required,
+ "boundary": "local eligibility is not installation authorization",
+}, ensure_ascii=False, indent=2))发布证据要绑定策略快照、候选和设备回执
NIST SP 800-218 SSDF 强调保留来源、构建、验证和变更证据。对应应用内更新,证据包应包含策略版本与签名、到期时间、渠道条件、APK 摘要、签名证书、构建来源、PackageInstaller 会话、历史升级基线、设备矩阵和回滚条件。
OWASP MASVS-RESILIENCE 将抗逆向与抗篡改定位为纵深控制。VMP 规则、方法清单和静态审计可以支持范围判断,但不证明某个候选已阻断修改,也不能替代远端发布控制。没有真实候选和回执时,不写客户、性能、兼容、攻击阻断、排名或收录结论。
若业务还有独立离线授权,可查看[离线许可逻辑的 VMP 范围](/zh-cn/articles/offline-license-logic-vmp-scope/),不要把更新资格缓存当作许可。需要提交候选和更新策略,可从[御盾中央平台申请加固服务](https://www.leonadev.com/console/)。登录、注册、价格、购买和控制台统一由中央平台承接。
- 本地展示、远端资格和平台安装状态使用不同类型
- versionCode 参与比较,versionName 不承担资格判断
- 渠道、灰度、撤回和最低版本由远端策略控制
- 策略元数据具有签名、单调版本、过期和目标摘要
- APK 与 split 的包名、版本和签名身份由平台验证
- VMP 仅覆盖小型策略映射与失败关闭分支
- 安装、升级和业务启动分别保留真实设备回执
- 所有证据绑定同一策略快照与最终候选摘要
事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| Android 更新要求保持 applicationId、兼容签名或轮换证明并使用可接受 versionCode。 | How Android app updates work 说明 Android 应用更新的身份与版本条件。 | 平台更新条件不等于业务防重放,也不证明更新候选符合远端策略。 |
| 更新客户端应验证签名、版本、过期时间和一致快照以识别回滚、冻结和混搭风险。 | The Update Framework specification 描述更新元数据角色与客户端验证工作流。 | TUF 不直接规定移动端 VMP 范围或 APK 业务授权策略。 |
| split 安装会话要求 base、packageName、versionCode 与签名证书保持一致。 | Android PackageInstaller 描述完整与继承安装会话及 split 约束。 | API 约束不证明 Play 服务器最终生成的所有 split 已被抽样。 |
| 发布需要区分应用签名密钥、上传密钥、证书与 Play App Signing 责任。 | Sign your Android app 说明 Android 发布签名角色与密钥责任。 | 文档不能确认某个实际包使用了批准的生产证书。 |
| 安全发布应保留来源、构建、验证、变更和供应链风险证据。 | NIST SP 800-218 SSDF 给出组织级安全软件开发和发布实践。 | SSDF 不定义具体更新策略,也不能证明某个候选已安装成功。 |
| 移动端抗逆向与抗篡改是纵深防御,不能替代远端发布控制。 | OWASP MASVS-RESILIENCE 定位移动端 resilience 控制及其目标。 | 控制目录不证明某个候选达到具体防护强度。 |
| 渠道映射、策略状态转换与失败关闭是优先 VMP 候选。 | 工程判断:这些小方法直接影响客户端更新行为且可用固定输入复核。 | 是否纳入仍需真实调用图、性能、异常和兼容证据。 |
| 下载、元数据密码库和 PackageInstaller 不应因位于同一链就整体进入 VMP。 | 工程判断:扩大平台耦合范围增加回归,却不提升远端元数据真实性。 | 具体项目存在旁路证据时仍应按方法重新评审。 |
| 服务端必须独立执行最低允许版本、撤回和下载资格。 | 项目证据尚未接入:这是上线前必须验证的发布门禁,不表示当前系统已实现。 | 客户端强制弹窗和版本头不能单独证明旧版本无法访问服务。 |
| 本文不提供性能、客户、排名、收录、兼容或攻击阻断结论。 | 项目证据尚未接入:缺少候选、策略签名、设备矩阵和公开运营数据。 | 文章只给出可复核范围与门禁,不能作为具体产品效果证明。 |
工程常见问题
应用内显示可更新是否说明目标 APK 可以安装?
不说明。还需远端资格、目标摘要、签名连续性、包名版本和 PackageInstaller 真实结果。
versionName 能否直接用于判断新旧版本?
不建议。它主要用于展示,Android 更新顺序使用 versionCode,还要结合签名和远端发布策略。
强制更新弹窗做 VMP 后能否阻止旧客户端访问?
不能单独做到。服务端必须执行最低允许版本和接口兼容策略,本地弹窗只是官方客户端体验。
更新下载器应该整体放进 VMP 吗?
通常不应。优先保护渠道与策略映射,下载、密码库和 PackageInstaller 保持可测试并按平台规则验证。
为什么策略元数据需要过期时间和单调版本?
用于识别长期冻结和明显回滚,避免客户端无限沿用旧的 eligible 或下载地址。
split 更新只验证 base APK 是否足够?
不足。实际安装集合中的包名、versionCode、签名证书和目标摘要都要一致并由会话验证。
本地脚本通过是否表示更新资格可信?
不表示。脚本只验证公开样例字段和展示边界,真实策略签名、候选与安装仍需独立回执。
怎样证明 VMP 后更新逻辑没有语义漂移?
用相同策略向量对比保护前后输出,并结合过期、撤回、未知渠道、版本倒退和安装失败用例。