先看结论与判断条件
- 客户端可以保护应用自有的请求字段收集、规范化、摘要生成和调用状态机,但不能成为完整性 verdict 的最终解释者。
- 标准请求应靠近受保护操作生成,并用 requestHash 绑定稳定业务请求;服务端需要按同一字节契约重算后比较。
- 完整性响应应直接传给受控后端解密和验证,客户端不应根据本地解析结果决定高价值操作是否允许。
- 任何单一 verdict 都不是绝对信任、Root 判定或 VMP 配置证明,业务裁决要结合账号、操作、时效、重放状态和风险策略。
- VMP 适合小而稳定的应用自有关键函数,不宜盲目包裹 Play SDK、生成代码、序列化框架、回调桥接和界面层。
- 放行证据要绑定最终 APK、VMP 配置、请求摘要契约、服务端策略版本、失败路径和设备回归,不能只记录一次成功调用。
先确定客户端不是最终信任根
Play Integrity 能向后端提供与应用、设备和请求相关的平台信号,但客户端运行在用户控制的设备上,调用路径、内存状态、分支和网络请求都可能被观察或修改。VMP 可以增加分析与篡改成本,却不能让本地 `if (verdict) allow()` 变成可靠授权。正确边界是客户端负责收集受保护操作的必要字段、生成绑定摘要、请求完整性令牌并转发;后端负责解密、验证、核对摘要和做业务决定。
划分边界时,应先画出真实操作链:用户发起敏感操作,客户端生成业务请求,计算 requestHash,请求 Play Integrity,将令牌与业务请求一起提交,后端验证完整性响应并重算摘要,随后结合账号、库存、支付、设备历史或其他业务状态决定允许、降级、二次验证或拒绝。任何缺少请求绑定或服务端重算的流程,都可能让完整性信号与实际操作脱节。
本文只回答 Play Integrity 客户端调用链中哪些代码适合进入 VMP、哪些判断必须留在服务端,不扩写成通用反作弊架构或 Play 上架教程。没有真实候选、后端日志和策略回执时,不能宣称某个方案已经阻断篡改、识别 Root 或达到特定防护强度。平台信号、客户端加固和业务授权是三层不同证据,必须分别陈述。
| 处理层 | 主要责任 | 可受 VMP 保护的部分 | 不得承担的结论 |
|---|---|---|---|
| 业务界面 | 收集用户动作和展示结果 | 通常不作为核心范围 | 最终允许或拒绝 |
| 请求构造 | 固定字段和操作上下文 | 应用自有确定性构造器 | 证明请求真实 |
| 摘要生成 | 绑定业务请求字节 | 小型规范化与哈希封装 | 认证用户或阻止重放 |
| 完整性调用 | 向平台请求令牌 | 应用自有编排状态机 | 在本地解释最终风险 |
| 受控后端 | 解密验证、重算与裁决 | 不属于客户端 VMP | 把单一信号当绝对信任 |
| 业务系统 | 授权、库存、支付和审计 | 服务端访问控制 | 由客户端布尔值代替裁决 |
标准请求要靠近受保护操作
Play Integrity 标准请求支持用 requestHash 将完整性响应绑定到业务请求,并由服务端按相同输入重算核对。这个摘要应在受保护操作即将提交时生成,覆盖能够区分操作语义的稳定字段,例如操作类型、业务对象标识、金额或数量的规范化表示、会话挑战和服务端签发的时效上下文。摘要若在应用启动时预先生成,或只覆盖一个通用字符串,后续真实操作就可能缺少绑定。
客户端 VMP 适合保护应用自有的字段选择、确定性编码和摘要调用封装,因为这些函数通常较小、输入输出明确,且一旦被篡改会让请求绑定失去意义。但秘密不能因此长期放在客户端,业务授权规则也不能复制到本地。攻击者仍可能观察输入输出、重放合法结果或绕过整个调用,因此后端必须核对摘要、请求时效、一次性上下文和账号状态。
请求失败策略要在产品层预先定义。网络不可用、Play 服务异常、令牌超时或平台信号不足时,是重试、降级、要求二次验证还是拒绝,取决于操作价值和用户影响。客户端只负责报告明确状态并防止并发状态混乱,不能自行把异常转换成成功。高价值操作的失败开放会直接削弱后端策略,失败关闭也可能造成可用性问题,两者都需要服务端可观测和审计。
- requestHash 在受保护操作附近生成而非应用启动时预生成
- 摘要覆盖操作语义、对象、规范化数量与服务端上下文
- 客户端不持有长期业务秘密或最终授权表
- 令牌和业务请求在同一次后端提交中关联
- 失败、超时、取消和并发状态均有明确处理
- 降级或拒绝策略由服务端和产品风险共同定义
请求摘要必须建立跨端字节契约
客户端与服务端只有对同一字节序列计算摘要,requestHash 比较才有意义。直接对普通 JSON 字符串做哈希很容易出现属性顺序、空白、Unicode、转义和数字表示差异。RFC 8785 定义 JSON Canonicalization Scheme,用于获得可重复的 JSON 表示;项目也可以采用更窄的固定字段和长度前缀协议。无论选择哪种方式,协议版本、字段类型、排序、缺省值和字符编码都必须写成可测试契约。
确定性不等于安全。JCS 或固定编码只能解决两端重算同一字节的问题,不提供用户认证、令牌解密、重放控制、完整性 verdict 或业务授权。摘要输入还需要服务端签发或可核对的上下文,后端必须验证请求中的业务字段与实际数据库状态。若客户端可以任意选择价格并正确计算摘要,摘要一致只能证明令牌绑定了这个错误价格,不能证明价格被授权。
摘要契约应建立跨语言测试向量。每个向量保存结构化输入、规范字节的十六进制或摘要、协议版本与预期失败条件,覆盖 Unicode、空字符串、字段缺失、前导零语义、大小写和重复请求。测试向量可以进入 VMP 前后回归,确认虚拟化没有改变编码和哈希输出。任何协议变更需要服务端先支持版本协商,不能依赖客户端与后端恰好同时上线。
| 字段类型 | 规范化要求 | 服务端核对 | 常见错误 |
|---|---|---|---|
| 操作类型 | 固定枚举和大小写 | 允许的服务端动作 | 客户端自由字符串 |
| 业务对象 | 稳定标识与字符编码 | 对象存在和归属 | 使用展示名称 |
| 数量金额 | 无歧义整数单位或严格格式 | 服务端价格与限额 | 浮点字符串漂移 |
| 挑战上下文 | 服务端生成并限时 | 状态、用途与是否已用 | 客户端自造随机值 |
| 账号会话 | 绑定认证上下文 | 当前用户和权限 | 只信请求中的账号 |
| 协议版本 | 显式稳定版本 | 选择对应解码规则 | 静默改变字段顺序 |
VMP 只包裹应用自有的稳定关键路径
适合进入 VMP 的通常是应用自有、规模可控且语义稳定的函数,例如摘要字段白名单、确定性编码器、请求上下文组装、完整性调用状态转换和提交前的一致性校验。这些函数应具有明确输入输出,避免反射、动态代理、大量泛型序列化和复杂协程生成状态。先用调用图确认它们与敏感操作直接相关,再建立保护前后的测试向量、异常路径和性能基线。
不宜盲目虚拟化 Play Integrity SDK 本身、Play 服务 IPC、平台生成类、序列化框架、网络库、依赖注入生成代码和界面回调。第三方与生成代码的版本、反射、线程和异常契约复杂,扩大保护范围会增加兼容性与排障成本,却不改变服务端信任边界。应用可以保护包裹这些库的自有薄层,但要把平台对象转换、回调线程和取消语义留在清晰边界上。
保护范围还要防止把所有错误处理藏进一个大函数。若令牌请求、网络提交、重试、UI 提示和业务决策被整体虚拟化,出现超时或版本兼容问题时很难定位。更合理的方式是把确定性摘要和关键状态检查设为小单元,平台调用与传输保持可观测接口,服务端裁决完全不下沉。VMP 配置每次变化都要绑定最终候选与回归回执,不能以方法数或覆盖率代替效果证据。
- 保护对象属于应用自有代码且与敏感操作直接相关
- 函数输入输出稳定并具备跨端测试向量
- Play SDK、IPC、生成代码和序列化框架保持清晰边界
- 平台回调线程、取消和异常语义可观察
- 业务允许拒绝逻辑不复制到客户端
- VMP 配置变化绑定同一最终候选和设备回归
后端先验证响应再组合业务策略
Play Integrity 概览强调完整性信号应靠近受保护操作,并由后端解密、验证和决策。后端收到业务请求与完整性令牌后,应先调用受支持的验证流程,核对响应中的请求详情与服务端重算的 requestHash,再读取适用于当前业务的信号。只有请求绑定成立,平台信号才与这次操作建立关系;摘要不匹配、协议版本未知或上下文过期都应走明确失败路径。
完整性响应包含请求详情和多类平台信号,具体信号要按业务场景组合使用。任何单一 verdict 都不是绝对信任、Root 判定或 VMP 配置证明。低风险浏览、账号修改、虚拟资产转移和支付确认可以采用不同阈值与后续动作。服务端还应结合用户认证、设备历史、速率、金额、异常行为和可恢复性,输出允许、限制、二次验证或拒绝,而不是简单保存一个客户端布尔值。
策略要支持审计与温和上线。每次裁决保存策略版本、适用信号、请求摘要核对、业务上下文、动作和原因码,同时避免记录不必要的敏感原文。新规则可以先观察命中分布和误伤,再逐步执行,但观察数据只能来自真实系统。本文不提供任何项目通过率或攻击阻断数字,服务端团队需要用自己的合法数据确定阈值、豁免和恢复流程。
| 步骤 | 输入 | 失败处置 | 证据 |
|---|---|---|---|
| 响应验证 | 完整性令牌和平台验证结果 | 拒绝或受控重试 | 验证状态与时间 |
| 请求绑定 | verdict requestHash 与重算值 | 阻断当前操作 | 协议版本和摘要 |
| 时效重放 | 服务端挑战和使用状态 | 拒绝过期或已用上下文 | 挑战状态记录 |
| 信号解释 | 当前业务所需 verdict | 按策略降级或二次验证 | 信号与适用条件 |
| 业务授权 | 账号、对象、金额和权限 | 拒绝未授权操作 | 服务端事实 |
| 最终动作 | 风险策略和恢复路径 | 返回稳定原因码 | 策略版本与审计事件 |
重放、并发和失败路径不能交给本地分支
requestHash 绑定请求内容,不自动提供一次性语义。相同业务请求若在允许窗口内被多次提交,摘要仍可能相同。服务端需要挑战、操作标识、幂等键或状态机来判断一次性与重复执行,并将它们纳入摘要输入或后端关联。客户端可以帮助生成和传递字段,却不能独立宣布某个挑战未使用,因为本地存储与时间都可能被回滚或修改。
并发请求也会暴露错误绑定。若两个操作共享一个全局令牌变量,回调到达顺序变化可能让 A 操作携带 B 的令牌。客户端编排层应为每次操作创建不可变上下文,绑定 requestHash、业务请求、令牌任务和取消状态;提交后立即封闭,不允许 UI 或后台任务修改字段。VMP 可保护这个应用自有状态转换,但必须保留可测试的关联标识和错误日志,不能把竞态藏起来。
失败处理要覆盖令牌请求失败、超时、用户取消、应用进入后台、进程死亡、网络重试、后端验证失败、摘要不匹配和策略拒绝。每条路径都要明确是否可以使用同一挑战、是否需要重新计算摘要、是否允许重试业务操作以及用户看到什么。客户端不应在异常时跳过完整性检查继续高价值操作,后端也不应把所有失败都映射成不可恢复封禁。
- 服务端挑战或操作标识具备明确时效和使用状态
- 每次客户端操作拥有不可变请求与令牌上下文
- 并发回调不会跨操作复用令牌或摘要
- 进程死亡和网络重试遵守同一幂等策略
- 摘要不匹配不会降级成客户端本地放行
- 拒绝与故障返回稳定原因码和恢复路径
用确定性编码检查两端 requestHash 输入
下面的 Python 示例实现一个公开安全的固定字段摘要检查器。它使用协议版本和字段白名单,把 UTF-8 字符串按字段名、字节长度和值编码,再计算 SHA-256 和 URL 安全 Base64。服务端读取客户端提交的业务字段,用自己的挑战状态和认证账号替换不可信字段后重新构造同一对象,最后采用常量时间比较。示例不包含密钥、令牌、真实接口或绕过逻辑。
这段代码刻意不声称实现通用 RFC 8785。固定字符串字段契约适合字段数量较少的高价值操作,若项目必须传递一般 JSON,应使用经过验证的 JCS 实现,并建立跨语言测试向量。无论哪种编码,客户端与服务端都不能各自随意序列化。示例对缺失文件、未知字段、缺少必需字段、非字符串值、挑战不一致和摘要不匹配设置可达失败路径。
生产服务端还应验证完整性响应本身、请求时效、挑战状态、用户授权和业务对象,不应把 `digest_matches` 当作最终允许。客户端 VMP 只保护编码器和调用路径,提高直接替换的成本;攻击者仍可能提交自己选择但摘要一致的字段。服务端从数据库重取价格、权限和对象状态,才是防止客户端自报可信数据的关键。
from pathlib import Path
import base64
import hashlib
import hmac
import json
import sys
if len(sys.argv) != 3:
raise SystemExit("usage: verify_request_hash.py request.json context.json")
def load(path_text):
path = Path(path_text).resolve()
if not path.is_file():
raise SystemExit(f"input missing: {path.name}")
return json.loads(path.read_text(encoding="utf-8"))
required = ("protocol", "action", "object_id", "amount_units", "challenge")
allowed = set(required) | {"request_hash"}
def canonical_bytes(payload):
unknown = set(payload) - allowed
if unknown:
raise SystemExit(f"unknown request fields: {sorted(unknown)}")
pieces = []
for key in required:
value = payload.get(key)
if not isinstance(value, str) or not value:
raise SystemExit(f"required string field missing: {key}")
raw = value.encode("utf-8")
pieces.append(f"{key}:{len(raw)}:".encode("ascii") + raw)
return b"|".join(pieces)
def request_hash(payload):
digest = hashlib.sha256(canonical_bytes(payload)).digest()
return base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ascii")
request = load(sys.argv[1])
context = load(sys.argv[2])
expected_challenge = str(context.get("active_challenge") or "")
if request.get("challenge") != expected_challenge:
raise SystemExit("challenge is absent, expired, or belongs to another operation")
provided = str(request.get("request_hash") or "")
computed = request_hash(request)
if not provided or not hmac.compare_digest(provided, computed):
raise SystemExit("request hash mismatch")
result = {
"digest_matches": True,
"protocol": request["protocol"],
"action": request["action"],
"next_checks": ["integrity-response", "challenge-state", "business-authorization"],
}
print(json.dumps(result, ensure_ascii=False, indent=2))发布证据要同时覆盖客户端与服务端
移动端抗篡改和抗逆向属于纵深防御控制,不能替代服务端授权和完整发布链。验收 VMP 范围时,应保存最终 APK 摘要、受保护方法清单、配置摘要、调用图边界、确定性测试向量、异常和并发回归,以及未保护的第三方与生成代码边界。结论只能写到实际测试范围,不能根据配置文件存在就声称客户端无法被修改。
安全软件开发还要求把来源、构建、验证、变更和供应链风险纳入持续流程。对应到 Play Integrity,服务端应保存摘要协议版本、平台验证方式、裁决策略版本、挑战生命周期、原因码和变更审核;客户端发布则保存 SDK 版本、构建变体、VMP 范围与设备矩阵。任何一侧变更都要触发契约测试,避免客户端成功升级却与旧后端序列化不一致。
一份可放行的回执至少覆盖成功、摘要不匹配、过期挑战、重复提交、平台信号不足、网络超时、并发错序和服务端拒绝。需要评估实际调用链时,可通过御盾中央平台提交脱敏的请求字段契约、客户端方法边界、后端重算流程和最终候选摘要,先确定 VMP 范围,再建立跨端测试。没有真实回执时,不登记完整性通过、攻击阻断或业务安全结论。
- 最终 APK 与 VMP 配置、方法清单和测试向量绑定
- 摘要协议和后端裁决策略均有明确版本
- 成功、失败、超时、重放和并发路径都有回执
- 第三方 SDK、生成代码和平台 IPC 边界被明确排除
- 服务端验证、请求绑定和业务授权分别记录
- 发布结论不把平台信号或客户端保护写成绝对可信
事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| 标准完整性请求可用 requestHash 把响应绑定到稳定序列化后的业务请求,并由服务端重算核对。 | Play Integrity standard requests 说明标准请求和 requestHash 的请求绑定方法。 | 摘要一致不等于用户已授权、请求未重放或业务字段来自可信来源。 |
| 完整性信号应靠近受保护操作请求,并由后端解密、验证和决策。 | Play Integrity overview 说明完整性调用在客户端与后端之间的责任分配。 | 平台信号不证明 VMP 配置、Root 状态或攻击已被阻断。 |
| 完整性响应包含请求详情和多类平台信号,后端应按业务场景组合使用。 | Play Integrity verdicts 说明响应字段和 verdict 的业务解释边界。 | 任何单一 verdict 都不是绝对信任、Root 判定或 VMP 配置证明。 |
| JSON 摘要输入需要固定可重复的属性排序、字符串和数字表示规则。 | RFC 8785 JSON Canonicalization Scheme 定义可重复的 JSON 规范化表示。 | JCS 只解决确定性表示,不提供认证、重放控制、完整性信号或业务授权。 |
| 移动端抗篡改与抗逆向属于纵深防御控制。 | OWASP MASVS-RESILIENCE 给出移动端抗篡改和抗逆向控制类别。 | 控制目录不能证明某个候选包达到任何具体防护强度,也不能替代服务端授权。 |
| 安全发布应保留来源、构建、验证和变更证据,并纳入供应链风险。 | NIST SP 800-218 SSDF 给出组织级安全软件开发与供应链实践。 | SSDF 不定义 Play Integrity 集成细节或某个 App 加固产品的具体能力。 |
| 客户端 VMP 应聚焦应用自有的请求构造、摘要和状态校验,不承担最终业务裁决。 | 工程判断:小型确定性函数适合建立保护前后测试,而授权下沉会让可修改客户端成为信任根。 | VMP 只能增加客户端分析与篡改成本,不能阻止所有绕过、重放或伪造业务输入。 |
| 摘要、完整性验证、重放状态和业务授权应作为服务端独立门禁逐层记录。 | 工程判断:分层原因码和策略版本可以区分请求绑定失败、平台信号不足与业务拒绝。 | 具体阈值、降级与恢复动作需要依据项目风险和真实数据确定。 |
工程常见问题
把 Play Integrity verdict 解析代码放进 VMP 就能本地裁决吗?
不能。客户端可被观察和修改,完整性响应应交给受控后端验证,最终允许或拒绝必须结合服务端业务状态。
requestHash 一致是否证明业务请求可信?
不证明。它只绑定规范化输入,后端仍要验证挑战、用户、对象、价格、权限、重放状态和完整性响应。
Play Integrity SDK 本身是否应该整体虚拟化?
通常不宜。优先保护应用自有的薄封装、确定性摘要和关键状态检查,平台 SDK、IPC、生成代码和回调保持清晰边界。
客户端与服务端直接对 JSON 字符串哈希可以吗?
只有两端严格共享同一规范化规则才可靠。一般 JSON 序列化会因顺序、数字、转义和 Unicode 不同产生摘要漂移。
requestHash 能否单独阻止重放?
不能。服务端还要维护挑战、操作标识、时效、使用状态和幂等规则,相同请求内容可能产生相同摘要。
准备 Play Integrity VMP 边界评审需要哪些材料?
准备最终 APK、客户端调用图、请求字段与摘要契约、后端验证和裁决流程、挑战策略、VMP 方法清单及失败回归用例。