先看结论与判断条件
- VMP 配置是可改变最终产物的构建输入,不是发布前临时点击记录;未版本化配置会让同一源码产生无法解释的候选差异。
- 配置身份至少包含 schema 版本、保护方法、排除项、策略参数、工具版本、构建 variant、责任人和文件摘要。
- build type、product flavor、source set、applicationId 和签名配置组合为不同 variant,不能让一份未限定配置静默覆盖全部变体。
- R8 的缩减、优化和混淆会改变编译后符号与可达性,VMP 选择器必须绑定处理顺序、规则和映射输出。
- provenance 与 attestation 应同时引用配置摘要和候选 APK 摘要,降低配置报告与实际交付文件错配的风险。
- 配置差异门禁要分别处理新增、删除、选择器漂移、工具变化和例外过期,但通过门禁不等于保护强度得到证明。
配置会改变候选产物,所以它属于发布输入
VMP 配置通常包含类或方法选择器、保护策略、排除项和工具参数。即使源码提交、依赖锁和 Gradle 任务完全相同,只要配置变化,最终 DEX、Native 载荷、调用路径或调试材料就可能不同。若团队只保存 APK 和源码提交,后续无法解释为什么某个方法被纳入、为什么某个入口被排除。
版本化不只是把文件放进 Git。配置记录还要包含 schema 版本、配置文件摘要、适用模块和 variant、VMP 工具及插件版本、生成方式、审批人和变更理由。发布时把这些字段冻结为不可混淆的输入集合,任何手工界面调整也必须导出为可比较配置,不能只留截图。
配置版本与产品版本可以分开。一个应用版本可能因紧急修复生成多个候选,配置也可能在同一业务版本中调整。工程判断是使用独立 configId 与 SHA-256,再在发布清单中引用源码提交、variant 和候选 APK 摘要。这样回滚配置时不会误把应用版本号当作规则内容。
| 字段 | 回答的问题 | 异常信号 | 发布动作 |
|---|---|---|---|
| configId 与摘要 | 使用了哪份精确规则 | 同 ID 摘要不同 | 停止并重建记录 |
| schemaVersion | 字段如何解释 | 工具升级后旧字段静默忽略 | 先迁移再构建 |
| toolVersion | 由哪个处理器执行 | 开发机与流水线版本不同 | 锁定并保存工具身份 |
| variant | 配置作用于哪个产物组合 | release 规则进入其他 flavor | 拆分适用范围 |
| 审批与理由 | 谁接受了变更及原因 | 临时排除永久保留 | 补到期与复核 |
保护清单要使用稳定资产标识和编译后证据
业务团队通常按授权校验、离线算法或交易规则命名资产,VMP 工具却按类、方法、签名和编译后符号工作。配置应同时保存 assetId、源码入口、编译后选择器、所属模块、保护策略和回归用例。只记录类名会在重构、Kotlin 生成代码或编译器升级后产生静默漂移。
新增和删除方法都需要解释。新增可能来自新资产、编译器生成方法或选择器扩张;删除可能是重构、裁剪、签名变化或配置漏配。差异脚本只负责找出变化,业务负责人要根据候选扫描和映射材料判断是合理迁移还是保护范围丢失。没有编译后证据时不能把配置命中当作事实。
排除项也属于保护清单。启动编排、反射桥、第三方 SDK 或异常恢复可能因兼容性需要排除,但每个排除应写清原因、适用范围、负责人和复核触发器。把排除规则散落在命令行或个人工作目录,会让后续版本继承未知风险。版本化清单应让排除与纳入同样可审计。
| 字段 | 保护项用途 | 排除项用途 | 复核依据 |
|---|---|---|---|
| assetId | 连接业务资产 | 说明被豁免资产 | 威胁模型与责任人 |
| selector | 选择目标方法 | 定义不处理范围 | 候选方法清单 |
| reason | 说明保护收益 | 说明兼容或故障边界 | 变更评审 |
| tests | 绑定业务语义回归 | 证明排除后的风险控制 | 同候选测试结果 |
| expiresOn 或 trigger | 安排策略复核 | 防止临时例外永久化 | 版本或工具变化 |
Android Build Variant 必须进入配置作用域
Android build variants 说明 build type、product flavor、source set、applicationId 和签名配置会组合成不同变体。不同 variant 可能包含不同源码、资源、SDK、组件和包名,保护目标自然也可能不同。一份以源码目录为前提的配置如果不声明 variant,容易命中不存在的方法或漏掉专属资产。
配置可以共享基线,但合并规则必须确定。公共敏感资产放在 base config,特定 flavor 和 build type 使用显式 overlay;流水线在构建前展开最终配置,计算摘要并输出来源链。禁止依靠文件系统遍历顺序或开发机环境变量决定覆盖关系,因为同一输入在其他环境可能得到不同结果。
签名配置虽然不决定方法选择,却决定候选身份和发布用途。debug、internal、release 或区域渠道的 VMP 结果不能互相冒充。工程判断是让每条发布记录同时包含 variant 名、applicationId、versionCode、签名阶段和 APK 摘要。相同源码仓库不代表不同 variant 具有相同运行行为。
| 维度 | 可能变化 | 配置风险 | 门禁 |
|---|---|---|---|
| build type | 优化、调试和签名设置 | debug 规则进入 release | 限制允许类型 |
| product flavor | 源码、资源与包名 | 漏掉专属敏感逻辑 | 展开 flavor overlay |
| source set | 类和组件集合 | 选择器命中漂移 | 比较候选符号 |
| applicationId | 应用交付身份 | 回执绑定错误产品 | 记录最终包名 |
| 签名阶段 | 测试与发布身份 | 测试候选冒充商业候选 | 绑定摘要与证书回执 |
R8 处理顺序会改变 VMP 选择器看到的世界
Enable app optimization with R8 说明 R8 承担代码和资源缩减、优化与名称混淆,发布构建需要保留对应规则和输出。VMP 与 R8 的先后顺序、方法内联、名称变化和不可达代码删除,会影响配置选择器是否仍命中目标。项目不能只保存 VMP 规则,还要保存 R8 配置、mapping 和相关输出身份。
若 VMP 在优化后工作,选择器应基于处理后的符号或稳定映射;若在优化前工作,仍要证明受保护方法经过后续处理后存在于最终候选。两种架构没有通用优劣,关键是流程固定且可复核。工具升级、插件顺序或 Gradle 任务关系变化都应触发配置重新验证。
R8 混淆不等于 VMP,也不证明抗动态分析能力。它们解决不同工程问题,回执也应分开。工程判断是把 R8 规则摘要、mapping 摘要、VMP 配置摘要、工具版本和最终 DEX 扫描放入同一候选证据包,但不把任一静态存在性结论写成实际保护强度。
| 输入 | 作用 | 漂移后果 | 保存证据 |
|---|---|---|---|
| R8 规则 | 缩减、优化和混淆约束 | 方法被删除或重命名 | 规则与摘要 |
| mapping 输出 | 连接源码与处理后符号 | 报告无法反查资产 | mapping 摘要和受控归档 |
| 任务顺序 | 定义工具看到的中间产物 | 相同规则命中不同对象 | 构建图与日志 |
| VMP 配置 | 选择方法和策略 | 保护范围变化 | 配置摘要与差异 |
| 最终 DEX 扫描 | 确认候选实际方法 | 配置意图未落到产物 | 候选摘要和命中表 |
provenance 要把配置摘要列入构建输入
SLSA Provenance v1.1 把产物 subject 与 builder、buildType、外部参数和依赖材料关联。VMP 配置可以作为构建参数或材料进入 provenance,最终 APK 摘要作为 subject。评审者由此能确认某份候选由哪个构建者、哪种流程和哪份配置产生,而不是依赖流水线名称猜测。
配置中可能含专有方法选择器,不适合公开完整内容。证明可以保存配置摘要、受控引用、schema 版本和非敏感统计,原文件留在权限受控归档。摘要匹配证明构建引用了同一字节,但不能证明选择器合理、工具实现正确或运行时保护强度达到某一等级。
外部参数也要限制。若流水线允许未记录的命令行覆盖、环境变量或临时 UI 设置,provenance 中的配置文件摘要仍不足以解释最终行为。工程判断是让所有影响保护范围的参数进入规范配置,构建在检测到未知覆盖时失败,并把展开后的最终配置摘要绑定候选。
| 对象 | 证明字段 | 核验问题 | 不能推出 |
|---|---|---|---|
| 最终 APK | subject digest | 证明是否对应当前候选 | 运行兼容通过 |
| VMP 配置 | parameter 或 material digest | 使用的是哪份规则 | 规则保护强度 |
| builder | 构建者身份 | 是否由认可流水线执行 | 内部实现无缺陷 |
| buildType | 流程类型 | 任务顺序是否匹配 | 业务测试已完成 |
| 工具与依赖 | materials 或受控引用 | 版本是否落在允许集合 | 候选没有漏洞 |
attestation 把差异审查与候选包绑定
in-toto Attestation Statement v1 使用 subject digest 和有类型 predicate 组织供应链声明。配置差异审查可以形成 predicate,列出 baseConfigDigest、candidateConfigDigest、新增、删除、漂移和审批结果,并把最终 APK 摘要放入 subject。这样报告无法轻易复制到同版本另一候选。
声明格式不保证内容真实。门禁仍需验证声明签名者是否有权批准配置、输入摘要是否匹配当前文件、差异是否由真实脚本产生,以及最终候选是否经过对应构建。若声明只引用应用版本号而没有 APK 摘要,仍存在重建错配空间。
多种审查可以分别声明。业务资产负责人确认保护清单,平台负责人确认工具和 variant,测试负责人确认设备回归,发布负责人确认候选身份。它们引用同一 subject,却保持不同 predicateType 和责任边界。工程判断是避免一枚“全部通过”签名掩盖每个角色实际审过什么。
| 字段 | 内容 | 审查者问题 | 失败状态 |
|---|---|---|---|
| 旧新配置摘要 | 两份精确规则身份 | 比较对象是否正确 | 摘要错配 |
| added 与 removed | 保护项集合变化 | 新增有测试、删除有理由吗 | 未解释变化 |
| changed | 策略或选择器字段漂移 | 变化是否影响故障半径 | 需要专项回归 |
| APK subject | 最终候选摘要 | 声明是否绑定交付文件 | 候选错配 |
| approver 与 predicateType | 责任与声明类型 | 签名者是否有授权 | 无权批准 |
SSDF 要求变更、验证和例外都有责任人
NIST SP 800-218 SSDF 提倡保留来源、构建、验证和变更证据,并管理供应链风险。VMP 配置治理可以据此定义变更流程:提出人说明资产与原因,工具负责人审核语法和兼容性,业务负责人确认保护范围,测试负责人绑定回归,发布负责人确认候选摘要。SSDF 不规定具体 VMP 功能。
差异门禁要对删除更敏感。新增保护项通常扩大范围,也可能增加故障半径;删除保护项可能暴露资产,也可能是合理重构。两者都不能自动放行。规则可以要求每个变化带 assetId、reason、owner、tests 和 review trigger,脚本发现缺失时阻断结构,最终风险仍由授权人员判断。
例外需要时间和触发条件。某个方法因兼容问题临时排除,应绑定当前 variant、工具版本和候选,并在方法重构、工具升级或指定日期触发复核。没有项目证据时,配置存在只能说明意图,不能声称候选已获得某种保护强度或攻击阻断结果。
- 配置变更有资产、理由和责任人
- variant overlay 合并顺序确定且可重现
- 工具与 R8 输入版本固定
- 删除、漂移和例外分别审批
- 配置摘要与 APK 摘要进入证明
- 设备回归绑定同一候选
- 配置存在不冒充保护强度
用只读脚本输出新增、删除和漂移项
下面的 Python 示例读取两个版本的公开安全 VMP 配置,验证 schema、configId、variant、toolVersion 和 rules,再按 assetId 比较规则。它分别输出 added、removed 和 changed,并对 variant 或工具版本变化标记 contextDrift。脚本不修改配置,也不包含真实客户类名、核心符号或内部路径。
changed 记录只输出发生变化的字段,不判断新策略更强或更弱。实际审批需要查看受控规则内容、候选方法清单和设备回归。若同一 assetId 在配置内重复,或必需字段缺失,脚本返回非零状态,防止覆盖造成静默丢失。输出摘要可进入 attestation。
准备 VMP 配置治理的软件加固评估时,可整理基线与候选配置摘要、工具版本、variant、R8 规则和 mapping 摘要、最终 APK、差异审批与设备回归范围,再通过御盾中央平台提交申请。所有结论必须绑定真实候选,不能用配置文件本身代替产物证据。
- 输入是受控导出的规范配置
- configId 与文件摘要同时保存
- assetId 在单份配置内唯一
- 新增、删除和字段漂移分别输出
- variant 与工具变化标记上下文漂移
- 差异结果不冒充保护强度
- 输出绑定候选 APK 后再审批
from pathlib import Path
import hashlib
import json
import sys
if len(sys.argv) != 3:
raise SystemExit(2)
baseline_path = Path(sys.argv[1])
candidate_path = Path(sys.argv[2])
if not baseline_path.is_file() or not candidate_path.is_file():
raise SystemExit(2)
baseline = json.loads(baseline_path.read_text(encoding="utf-8"))
candidate = json.loads(candidate_path.read_text(encoding="utf-8"))
required = ["schemaVersion", "configId", "variant", "toolVersion", "rules"]
if any(field not in baseline or field not in candidate for field in required):
raise SystemExit(2)
def index_rules(config):
rules = config.get("rules")
if not isinstance(rules, list) or not rules:
raise SystemExit(2)
indexed = {}
for rule in rules:
asset_id = str(rule.get("assetId", ""))
if not asset_id or asset_id in indexed:
raise SystemExit(2)
if not rule.get("selector") or not rule.get("strategy") or not rule.get("owner"):
raise SystemExit(2)
indexed[asset_id] = rule
return indexed
old_rules = index_rules(baseline)
new_rules = index_rules(candidate)
added = sorted(set(new_rules) - set(old_rules))
removed = sorted(set(old_rules) - set(new_rules))
changed = {}
for asset_id in sorted(set(old_rules) & set(new_rules)):
fields = sorted(key for key in set(old_rules[asset_id]) | set(new_rules[asset_id]) if old_rules[asset_id].get(key) != new_rules[asset_id].get(key))
if fields:
changed[asset_id] = fields
canonical = json.dumps(candidate, sort_keys=True, separators=(",", ":"))
result = {"status": "review-required" if added or removed or changed else "no-rule-drift", "baselineConfigId": baseline["configId"], "candidateConfigId": candidate["configId"], "candidateDigest": hashlib.sha256(canonical.encode("utf-8")).hexdigest(), "contextDrift": {"variant": baseline["variant"] != candidate["variant"], "toolVersion": baseline["toolVersion"] != candidate["toolVersion"]}, "added": added, "removed": removed, "changed": changed}
print(json.dumps(result, ensure_ascii=False, indent=2))事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| 构建证明可以绑定产物、构建者、构建类型、外部参数和依赖材料。 | SLSA Provenance v1.1 定义 provenance 的 subject、builder、buildType、parameters 与 materials。 | provenance 记录构建过程,不单独证明 VMP 策略合理或运行时安全。 |
| 供应链声明可以把候选 APK 摘要与有类型的配置审查结果绑定。 | in-toto Attestation Statement v1 定义 subject、predicateType 与 predicate 结构。 | 声明格式不保证内容真实,仍需可信签名者和输入摘要核验。 |
| 安全发布应保留来源、构建、验证和变更证据并管理供应链风险。 | NIST SP 800-218 SSDF 提供组织级安全软件开发实践。 | SSDF 不定义具体 VMP 工具、配置语法或保护强度。 |
| build type、product flavor、source set、applicationId 和签名配置组合成不同发布变体。 | Android build variants 描述 Android 构建变体和 source set 合并。 | 同一仓库不代表所有 variant 具有相同代码、资源、证书和运行行为。 |
| R8 承担代码和资源缩减、优化与名称混淆,并产生相应规则和输出。 | Enable app optimization with R8 说明 R8 在发布构建中的职责。 | R8 优化不等于 VMP,也不证明抗动态分析或实际保护强度。 |
| 依赖 Android 运行时和系统 API 的候选语义应通过设备端测试。 | Android instrumented tests 说明设备测试可访问真实 Android 框架能力。 | 单一设备通过不能代表完整 API、ABI 和厂商矩阵。 |
| VMP 配置必须作为版本化发布输入并绑定候选 APK 摘要。 | 工程判断:配置变化会改变保护范围与最终产物,缺少身份会导致回执错配。 | 版本化提高追溯性,不自动证明选择器有效或保护强度达标。 |
| 删除、选择器漂移和临时排除应有独立责任与复核触发器。 | 工程判断:这些变化可能让核心资产静默退出保护范围。 | 具体风险和放行决定仍需项目威胁模型、候选扫描与设备证据。 |
工程常见问题
VMP 配置放进 Git 就算完成版本化了吗?
不够。还要保存 schema、文件摘要、工具版本、variant、展开后的最终配置、审批和候选 APK 摘要,才能解释真实发布输入。
为什么同一份配置不能直接用于所有 Android variant?
不同 build type 和 flavor 可能拥有不同源码、资源、applicationId、SDK 与签名阶段。配置应声明作用域并核对最终候选符号。
R8 mapping 与 VMP 配置有什么关系?
R8 会缩减、优化和混淆代码,改变 VMP 选择器看到的符号。处理顺序、规则与 mapping 摘要应和 VMP 配置一起归档。
provenance 绑定配置摘要后是否能证明保护有效?
不能。它证明记录中的构建使用了某份输入,还要通过候选扫描、运行回归和项目安全评审判断选择器与实际效果。
配置删除一个方法时应该自动阻断吗?
至少应进入强制审查。删除可能是合理重构,也可能是保护丢失,必须连接 assetId、原因、候选符号、责任人和回归结果。
申请 VMP 配置治理评估前要准备哪些材料?
准备基线与候选配置、工具和 variant、R8 规则及 mapping 摘要、最终 APK、差异审批和设备回归范围,再从御盾中央平台提交申请。