先看结论与判断条件
- 共用部分应是稳定业务资产的基线,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 派生结果关联。差异层为空是一项需要证据支持的结论,不是默认配置。
| 层次 | 需要比较 | 可共用信号 | 必须拆分信号 |
|---|---|---|---|
| 业务资产 | 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 范围应聚焦自有关键逻辑及其必要调用边界,不应因为依赖存在就盲目处理整套第三方代码。是否保护第三方入口还受许可证、更新频率、反射和兼容风险约束。
| 维度 | 可能改变的对象 | 清单字段 | 失败条件 |
|---|---|---|---|
| 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 摘要与模块表 | 设备已安装全部代码 |
| 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 命中证据
- 缺失与专属资产分别解释
- 模块和功能上下文随差异保存
- 脚本结论不冒充保护效果
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 和设备测试范围,再从御盾中央平台提交申请。