先看结论与判断条件

  • 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 摘要。这样回滚配置时不会误把应用版本号当作规则内容。

VMP 配置作为发布输入的最小身份
字段回答的问题异常信号发布动作
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 具有相同运行行为。

Variant 维度对 VMP 配置的影响
维度可能变化配置风险门禁
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 与 VMP 组合需要冻结的输入
输入作用漂移后果保存证据
R8 规则缩减、优化和混淆约束方法被删除或重命名规则与摘要
mapping 输出连接源码与处理后符号报告无法反查资产mapping 摘要和受控归档
任务顺序定义工具看到的中间产物相同规则命中不同对象构建图与日志
VMP 配置选择方法和策略保护范围变化配置摘要与差异
最终 DEX 扫描确认候选实际方法配置意图未落到产物候选摘要和命中表

provenance 要把配置摘要列入构建输入

SLSA Provenance v1.1 把产物 subject 与 builder、buildType、外部参数和依赖材料关联。VMP 配置可以作为构建参数或材料进入 provenance,最终 APK 摘要作为 subject。评审者由此能确认某份候选由哪个构建者、哪种流程和哪份配置产生,而不是依赖流水线名称猜测。

配置中可能含专有方法选择器,不适合公开完整内容。证明可以保存配置摘要、受控引用、schema 版本和非敏感统计,原文件留在权限受控归档。摘要匹配证明构建引用了同一字节,但不能证明选择器合理、工具实现正确或运行时保护强度达到某一等级。

外部参数也要限制。若流水线允许未记录的命令行覆盖、环境变量或临时 UI 设置,provenance 中的配置文件摘要仍不足以解释最终行为。工程判断是让所有影响保护范围的参数进入规范配置,构建在检测到未知覆盖时失败,并把展开后的最终配置摘要绑定候选。

VMP 构建证明的连接对象
对象证明字段核验问题不能推出
最终 APKsubject 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 后再审批
比较两个版本 VMP 配置的只读差异检查器
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、差异审批和设备回归范围,再从御盾中央平台提交申请。

想用自己的 App 验证?

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

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