先看结论与判断条件

  • 共用部分应是稳定业务资产的基线,Variant 专属源码、依赖入口、功能开关和例外放进显式差异层,不能依靠通配符静默吸收变化。
  • 判断范围要看编译后的实际方法与模块,不以源码目录、产品名称或同一仓库作为资产相同的替代证据。
  • AAB 会按设备配置派生 APK 集,动态功能模块和配置拆分可能改变设备实际收到的代码,清单必须连接可交付模块证据。
  • debug、internal 与 release 的签名责任和交付用途不同,某一 Variant 的测试结果不能复制为另一 Variant 的发布回执。
  • 每份最终清单应绑定 variantId、构建参数、基线摘要、差异摘要、候选摘要、签名证书摘要与设备测试范围。
  • 交集与差集脚本只能发现范围漂移;是否纳入 VMP、能否排除以及保护效果如何,仍需威胁模型、兼容回归和真实候选证据。

先回答共用条件:共享基线,不共享未经证明的结果

Android 项目常把 release、internal、区域渠道或不同业务形态组合成多个 Build Variant。它们可以复用一份描述稳定核心资产的 VMP 基线,例如授权决策、离线规则解析和关键状态转换;但基线只是共同意图,不是每个候选都已命中的事实。最终范围必须在各 Variant 编译完成后重新展开和核对。

若两个 Variant 的源码集、依赖图、生成代码、功能开关、AAB 模块和签名阶段并不相同,一份扁平清单会制造两类故障:不存在的选择器被误认为已保护,真实存在的专属入口又因没有明确所有者而漏掉。通配符命中数量相同也不足以证明命中了同一业务资产,因为重构和编译优化会改变符号。

可执行的结构是 base 加 overlay。base 保存跨 Variant 稳定的 assetId、语义入口和最低回归契约;overlay 只声明增加、替换、排除及原因。流水线展开最终清单后计算摘要,再与该 Variant 的编译后资产、APK 或 AAB 派生结果关联。差异层为空是一项需要证据支持的结论,不是默认配置。

判断两个 Variant 是否能共用范围的四层证据
层次需要比较可共用信号必须拆分信号
业务资产assetId 与威胁目标语义和责任人相同业务规则或风险不同
编译输入源码集、依赖、生成代码、开关输入集合等价存在专属代码路径
交付结构base、feature 与配置模块设备可达模块相同动态功能或拆分不同
发布身份applicationId、签名阶段、候选摘要身份与用途一致测试包和商业包混用
验证范围设备、API、ABI 与用例证据覆盖同一矩阵仅一方有真实回执

Build Type、Product Flavor 与 Source Set 会改变资产集合

Android build variants 说明,build type、product flavor、source set、applicationId 和签名配置会组合为不同变体。source set 可以覆盖类、资源和清单,flavor 可以引入特定依赖,build type 还能改变调试、优化和签名设置。因此,同一 Git 提交并不能推出不同 Variant 含有同一组可执行方法。

清单建模应从业务资产向编译对象映射,而不是反向把类名当成资产。一个 assetId 可以在公共源码中实现,也可能在某个 flavor 中由另一实现替换。最终清单要记录 sourceOrigin、compiledSelector、module、variantId 和 contractId;若实现替换,overlay 必须明确替换关系,避免 base 和专属实现同时进入保护。

依赖也要进入判断。某个 Variant 可能启用支付、企业认证或离线能力,另一个完全没有对应 SDK 和桥接层。VMP 范围应聚焦自有关键逻辑及其必要调用边界,不应因为依赖存在就盲目处理整套第三方代码。是否保护第三方入口还受许可证、更新频率、反射和兼容风险约束。

Variant 维度对 VMP 清单的具体影响
维度可能改变的对象清单字段失败条件
build type优化、调试入口、签名阶段buildType 与用途debug 结果冒充 release
product flavor业务实现、依赖和资源flavor 与 assetId专属资产没有 overlay
source set类、清单和资源覆盖sourceOrigin选择器仍指向旧实现
applicationId安装与交付身份applicationId回执绑定错误产品
生成代码编译期桥接和序列化入口generator 与 selector工具升级造成静默漂移

把功能开关分成编译期差异和运行期路径

功能开关并不都以相同方式影响范围。编译期常量、资源选择、依赖开关和条件插件可能让代码根本不进入某个 Variant;服务端开关则可能让同一二进制包含多条路径,只在运行时决定可达性。前者改变资产集合,后者主要改变测试状态与威胁暴露,不能用一条 enabled 字段概括。

对编译期差异,流水线应读取最终 Variant 元数据和编译后清单,确认 assetId 对应的选择器确实存在。对运行期路径,VMP 清单通常保持目标方法不变,但回归契约要覆盖开关开启、关闭、迁移和失效回退。若保护后只验证默认状态,异常分支可能直到线上配置变化才暴露。

工程判断是让每个 overlay 写出 conditionType、conditionId、expectedPresence 和 testStates。expectedPresence 用于校验方法是否应该存在,testStates 用于列出必须执行的业务状态。不得把线上开关当前关闭当作长期排除理由;只要代码仍随候选交付且能被本地条件或未来配置触达,就要按项目威胁模型处理。

功能开关类型与范围决策
开关类型代码是否进入候选VMP 清单动作验证动作
编译期常量可能被裁剪声明预期存在性扫描编译后对象
依赖开关可能新增整组入口增加专属 assetId核对依赖和调用边界
资源或清单覆盖组件可达性改变更新入口映射执行组件级设备测试
服务端开关代码通常仍在包内保留目标范围覆盖开启、关闭与回退
实验分组同一包内多路径并存按交付代码建模验证状态转换与持久化

AAB 的模块和派生 APK 会改变设备实际收到的代码

Android App Bundle format 描述 AAB 由 base、feature、配置和资产模块组成,发布系统再按设备配置生成 APK 集。VMP 对 AAB 处理完成并不表示每台设备都得到完全相同的代码集合。动态功能模块可能按需或按条件交付,配置拆分还会根据 ABI、语言和密度选择不同分片。

因此范围清单需要记录 moduleName、deliveryMode、assetId 和处理后方法证据。公共基线若引用动态模块中的实现,base Variant 的扫描结果可能看不到该方法;反过来,只扫描完整 AAB 也可能把目标存在误写成设备已安装。工程判断是分别保存 bundle 级处理证据和代表性派生 APK 集的可达证据。

Build and test Android App Bundles 说明可以使用 bundletool 和测试轨道生成或验证派生 APK。团队应以同一 AAB 摘要、设备规格和模块条件生成测试对象,并记录生成命令与结果摘要。本地派生有助于复现,但不能替代应用商店线上交付回执,也不能证明所有设备组合都得到相同模块。

从 AAB 到设备交付的范围核验点
对象回答的问题需要保存不能推出
AAB构建提交了哪些模块AAB 摘要与模块表设备已安装全部代码
base 模块基础安装包含哪些入口处理后选择器清单动态模块范围一致
feature 模块专属资产如何交付交付条件与模块摘要所有用户均已获取
设备规格派生选择依据是什么ABI、语言、密度与 SDK覆盖全部设备组合
APK 集代表性设备实际获得什么生成回执与文件摘要线上交付完全相同

签名和交付身份决定回执能否跨 Variant 使用

Sign your Android app 区分应用签名密钥、上传密钥、证书和 Play App Signing 的责任。不同 Variant 常使用不同 applicationId、证书或签名阶段。即使 VMP 清单文本相同,internal 候选上的安装与回归回执也不能直接证明 release 候选,因为重新构建、重新处理或不同签名输入都会产生另一文件身份。

范围记录至少要绑定 artifactSha256、applicationId、versionCode、variantId、signingCertificateSha256 和 signingStage。若发布使用 AAB,还要区分上传 AAB、商店派生 APK 与本地测试 APK 的摘要链。证书摘要只说明签名者身份的一部分,不能证明包内容、密钥保管流程或 VMP 保护效果。

为降低测试成本,可以让多个 Variant 共用契约和设备矩阵设计,但每个商业候选仍要有独立身份回执。若团队使用同一 release 实现派生多个渠道包,需要证明构建参数、模块、资源和处理配置差异,不能根据版本名相同合并记录。候选摘要不一致时,默认视为不同验收对象。

签名与候选身份的最小绑定
字段用途错配信号处理
artifactSha256锁定精确候选文件报告摘要与文件不同停止复用回执
variantId连接构建组合同名配置指向多种组合记录完整维度
applicationId确认安装和交付身份测试包名不同分开验收
证书摘要确认签名证书身份debug 证书进入发布记录拒绝候选
signingStage区分上传与最终签名把上传密钥当应用签名密钥补齐责任链

用 provenance 固定基线、overlay 与最终候选的关系

SLSA Provenance v1.1 使用 subject、builder、buildType、外部参数和 materials 描述构建。VMP 范围治理可以把 base 清单、Variant overlay、工具版本、依赖锁和构建参数作为材料或参数,把最终 APK 或 AAB 摘要作为 subject。这样评审者能判断某份范围报告是否属于当前候选。

provenance 不要求公开敏感方法名称。公开或跨团队回执可以保存清单摘要、schema、variantId、非敏感统计和受控证据引用,详细选择器留在权限受限位置。摘要一致证明引用的是同一字节序列,但不证明清单选择合理、工具实现正确或候选具备某种攻击阻断能力。

合并规则本身也要确定。base 与 overlay 的优先级、允许操作和冲突处理必须由 schema 定义,流水线在未知字段、重复 assetId、无理由删除或 Variant 身份不匹配时失败。展开后的 finalScopeDigest 比源文件摘要更接近真实输入,因为它包含了继承和替换后的最终决策。

范围证明应连接的对象
证明对象建议字段审查问题边界
公共基线baseScopeDigest共同资产是否固定不代表全部 Variant 命中
差异层overlayDigest 与理由专属增删是否可解释不判断保护强弱
展开范围finalScopeDigest实际输入是否可重现仍需核对编译对象
构建者builder 与 buildType是否由认可流程生成不保证工具无缺陷
候选产物subject digest报告是否绑定交付文件不证明设备兼容

设备测试必须覆盖 Variant 专属入口和失败条件

Android instrumented tests 允许测试代码在真实 Android 运行时中访问组件和系统 API。VMP 改变方法执行方式后,纯 JVM 测试不能覆盖 Binder、ClassLoader、组件生命周期、Keystore 调用或厂商运行时差异。每个 Variant 应从最终范围反向生成必须验证的业务契约与组件入口。

测试矩阵不必机械复制所有用例。公共基线资产可以复用契约定义和输入数据,Variant overlay 则增加专属入口、异常、副作用和开关状态。执行结果必须绑定精确候选摘要与设备身份。单一设备通过不能代表完整 API、ABI、系统版本和厂商矩阵,也不能替代线上观测。

放行条件应同时回答范围命中和语义一致。静态扫描确认 finalScope 中的选择器落在候选;设备测试确认指定输入、输出、异常和允许副作用满足契约;AAB 测试确认代表性派生 APK 含有预期模块。任何一层缺失都应标为待证,而不是用另一层结果补写结论。

  • 公共资产拥有可跨 Variant 复用的语义契约
  • 专属 overlay 生成额外设备用例
  • 结果绑定候选摘要、设备与 Variant
  • 动态模块按交付条件验证
  • 异常和副作用有显式断言
  • 单设备通过不外推完整矩阵
  • 静态命中与运行语义分开记录

用交集与差集校验器阻止范围静默漂移

下面的 Python 示例读取一份 Variant 元数据数组。每条记录包含 variantId、applicationId、artifactSha256、signingCertificateSha256、modules、features 和 protectedAssets。脚本验证字段、摘要格式与 assetId 唯一性,计算全部 Variant 的资产交集,并逐个输出专属资产与缺失资产。它只分析公开安全的元数据。

校验器要求至少两个 Variant,并把第一条记录作为已批准比较基准。若某个 Variant 少了基准资产,或出现未登记在 features 或 modules 上下文中的专属资产,结果进入 review-required;结构或摘要非法则返回非零状态。脚本不自动决定保护策略,输出只用于让负责人解释差异。

准备 Build Variant 的 VMP 范围评估时,可整理 Variant 维度、base 与 overlay 摘要、编译后资产、AAB 模块、候选文件、证书摘要、派生 APK 和设备契约回执,再通过御盾中央平台提交申请。范围校验通过只证明元数据满足既定规则,不证明完整兼容或实际保护强度。

  • 输入元数据绑定真实候选摘要
  • variantId 与 applicationId 明确
  • 资产列表无重复且可追溯
  • 公共交集不替代 Variant 命中证据
  • 缺失与专属资产分别解释
  • 模块和功能上下文随差异保存
  • 脚本结论不冒充保护效果
读取 Variant 元数据并比较 VMP 资产交集与差集
from pathlib import Path
import json
import re
import sys

if len(sys.argv) != 2:
    raise SystemExit(2)
input_path = Path(sys.argv[1])
if not input_path.is_file():
    raise SystemExit(2)
variants = json.loads(input_path.read_text(encoding="utf-8"))
if not isinstance(variants, list) or len(variants) < 2:
    raise SystemExit(2)
required = ["variantId", "applicationId", "artifactSha256", "signingCertificateSha256", "modules", "features", "protectedAssets"]
sha_pattern = re.compile(r"^[0-9a-f]{64}$")
asset_sets = {}
contexts = {}
for item in variants:
    if any(field not in item for field in required):
        raise SystemExit(2)
    variant_id = str(item["variantId"])
    if not variant_id or variant_id in asset_sets:
        raise SystemExit(2)
    if not sha_pattern.fullmatch(str(item["artifactSha256"]).lower()):
        raise SystemExit(2)
    if not sha_pattern.fullmatch(str(item["signingCertificateSha256"]).lower()):
        raise SystemExit(2)
    assets = item["protectedAssets"]
    if not isinstance(assets, list) or not assets or len(assets) != len(set(assets)):
        raise SystemExit(2)
    asset_sets[variant_id] = set(assets)
    contexts[variant_id] = set(item["modules"]) | set(item["features"])
variant_ids = list(asset_sets)
common_assets = set.intersection(*(asset_sets[name] for name in variant_ids))
baseline_id = variant_ids[0]
baseline_assets = asset_sets[baseline_id]
reports = {}
review_required = False
for variant_id in variant_ids:
    missing = sorted(baseline_assets - asset_sets[variant_id])
    exclusive = sorted(asset_sets[variant_id] - common_assets)
    if missing or (exclusive and not contexts[variant_id]):
        review_required = True
    reports[variant_id] = {"missingFromBaseline": missing, "variantExclusive": exclusive, "context": sorted(contexts[variant_id])}
result = {"status": "review-required" if review_required else "scope-consistent", "baselineVariant": baseline_id, "commonAssets": sorted(common_assets), "variants": reports}
print(json.dumps(result, ensure_ascii=False, indent=2))

事实依据与适用边界

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

本文判断事实或工程依据适用限制
build type、product flavor、source set、applicationId 和签名配置能够组合为不同 Android 构建变体。Android build variants 描述构建变体、源码集合和相关配置组合。同一源码仓库不代表不同 Variant 具有相同依赖、资源、证书或运行行为。
AAB 包含 base、feature、配置和资产模块,设备安装对象是按配置派生的 APK 集。Android App Bundle format 描述 Bundle 模块结构和派生交付格式。AAB 不是设备直接安装的最终 APK,完整 Bundle 中存在代码也不等于设备已收到。
bundletool 和测试轨道可以帮助重现并检查 AAB 派生 APK 的交付行为。Build and test Android App Bundles 说明本地与测试轨道的 Bundle 测试方法。本地生成结果不能完全替代 Play 线上交付回执或覆盖全部设备组合。
构建证明可以把候选产物与构建者、构建类型、参数和材料绑定。SLSA Provenance v1.1 定义 subject、builder、buildType、parameters 与 materials。provenance 记录构建关系,不单独证明 VMP 策略正确、运行兼容或保护强度。
依赖 Android 运行时、组件和系统 API 的候选语义应通过设备端测试。Android instrumented tests 说明设备测试可访问真实 Android 框架能力。单一设备通过不能代表完整 API、ABI、系统版本和厂商矩阵。
发布流程需要区分应用签名密钥、上传密钥、证书和 Play App Signing 的责任。Sign your Android app 描述 Android 签名、证书和 Play App Signing 流程。文档不能确认某个实际候选使用了正确生产证书,也不能证明包内容安全。
不同 Variant 应共享稳定资产基线,并用显式 overlay 表达专属增删和替换。工程判断:编译输入与交付结构差异会使扁平清单产生漏配、误配和证据错配。base 加 overlay 提高可追溯性,不自动证明最终选择器已命中候选。
VMP 范围通过必须同时具有编译后命中证据、候选身份和设备语义回执。工程判断:配置意图、产物存在和运行行为属于不同证据层,不能彼此替代。具体测试矩阵和放行标准仍需项目威胁模型、交付渠道与兼容要求确认。

工程常见问题

release 和 internal Variant 可以直接共用一份 VMP 清单吗?

可以共用稳定资产基线,但要分别展开最终范围并绑定各自源码集、依赖、签名阶段和候选摘要。任何差异都应进入显式 overlay。

两个 Variant 的保护方法数量相同,是否说明范围一致?

不能。数量相同可能对应不同方法或不同业务语义,需要比较 assetId、编译后选择器、模块来源和候选命中证据。

服务端功能开关关闭时可以从 VMP 清单删除对应逻辑吗?

不能仅凭当前关闭删除。若代码仍随候选交付并可能被未来配置或本地状态触达,应按威胁模型保留范围并覆盖开关状态测试。

完整 AAB 中看到受保护方法是否足以证明设备会收到它?

不足。动态功能和配置拆分会影响设备 APK 集,需要结合交付条件、设备规格、派生 APK 和线上或测试轨道回执判断。

internal 包设备测试通过后能否复用到 release 包?

契约和测试设计可以复用,结果不能直接复用。release 候选具有独立文件摘要、签名阶段和构建输入,需要自己的身份与执行回执。

申请 Build Variant 范围评估前需要准备什么?

准备 Variant 维度、base 与 overlay、编译后资产、AAB 模块、候选摘要、证书摘要、派生 APK 和设备测试范围,再从御盾中央平台提交申请。

想用自己的 App 验证?

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

继续阅读: VMP 加固是什么,如何选择保护范围