先看结论与判断条件
- 完整性请求失败、超时、无 verdict、设备不支持、后端验证失败和明确风险必须使用不同状态,不得合并为 false。
- 完整性信号应靠近受保护操作生成,并与稳定业务请求摘要绑定,由后端解密、核对和决定。
- VMP 优先保护状态到业务动作的映射、缓存边界和禁止默认放行分支,不整体包住平台 SDK。
- 缓存只能在明确操作、主体、请求摘要、策略版本和短有效期内复用,未知年龄或上下文变化立即失效。
- 低风险读取、可恢复写入、资金与权限操作应采用不同降级策略,但最终允许仍由服务端根据业务状态裁决。
- 审计门禁要穷举状态与操作组合,确保未知状态、异常抛出和新增枚举都落入拒绝或重新确认路径。
直接答案:把不可用拆成状态,不建立万能 fallback
完整性信号不可用不是单一事件。SDK 初始化失败、请求超时、设备或账号不受支持、响应为空、后端解密失败、requestHash 不匹配和明确风险 verdict 的证据含义不同。状态机先保留原因,再结合操作风险选择重试、只读、延迟提交或拒绝,不能统一映射为 allow 或 deny。
VMP 最适合保护的是这张状态到动作的映射、缓存使用条件、重试上限和禁止降级分支。平台 SDK 初始化、网络传输、响应解密与服务端业务授权保持独立。这样保护范围较小,能够用完整输入矩阵回归,也避免 VMP 异常影响系统服务和网络栈。
最终裁决必须在后端。客户端上的 integrityOk、cachedVerdict 或 unsupportedDevice 都可能被修改,后端应验证平台响应、请求绑定、主体、时间和业务对象,再按服务器策略返回有限动作。客户端即使绕过界面,也不能让敏感 API 因缺少信号而默认放行。
| 状态 | 观察事实 | 客户端动作 | 服务端动作 |
|---|---|---|---|
| timeout | 期限内无响应 | 有限重试或延后 | 不视为通过 |
| unsupported | 环境不支持 | 提示替代路径 | 按业务风险评估 |
| no-verdict | 缺少所需信号 | 不缓存通过 | 要求重新取证 |
| verification-error | 后端验证失败 | 清除临时结果 | 保持拒绝并记录 |
| hash-mismatch | 响应未绑定请求 | 废弃响应 | 拒绝操作 |
| explicit-risk | 平台返回风险信号 | 停止敏感操作 | 组合策略裁决 |
信号获取、平台 verdict 和业务裁决是三层证据
Play Integrity overview 建议让完整性请求靠近受保护操作,并由后端解密、验证和决策。客户端成功调用 SDK 只说明拿到某种响应,不代表响应属于当前业务请求,也不代表服务器接受。日志和类型要区分 request-created、token-received、verified 与 business-decided。
Play Integrity verdicts 包含请求详情和多类平台信号,后端应按场景组合使用。任何单项值都不是绝对可信根,也不能被公开写成 Root 判定、VMP 配置证明或攻击已被阻断。业务策略要说明读取了哪些字段、证据缺失时如何处理以及哪些操作仍需额外认证。
服务端响应也不应压成永久 trusted。它应绑定主体、操作、对象、策略版本和短有效期,并返回 allow、deny、retry 或 limited 等明确枚举。客户端只消费这一结果,不自行从原始 verdict 推导更宽权限,避免不同版本各自实现一套不可审计的授权逻辑。
| 层级 | 输入 | 输出 | 禁止推论 |
|---|---|---|---|
| SDK 请求 | 业务上下文 | 平台响应材料 | 设备可信 |
| 平台 verdict | 请求详情与信号 | 多项观察 | 业务允许 |
| 后端验证 | 平台响应和 requestHash | 已验证证据 | 对象授权 |
| 业务裁决 | 主体、对象、风险策略 | 有限动作 | 永久可信 |
| 客户端展示 | 服务端动作 | 界面与重试 | 服务器授权 |
| VMP 保护 | 状态机方法 | 提高修改成本 | 信号真实性 |
requestHash 绑定必须稳定、可重算且留在服务端核对
标准完整性请求可使用 requestHash 把响应与稳定序列化的业务请求绑定。客户端应在受保护操作附近选择必要字段,使用固定字节规则计算摘要;后端从自身接收的请求重建同一输入并比较。若双方仅对一个可被替换的 requestId 求摘要,绑定无法覆盖真实业务对象。
规范化规则需要版本化,明确字段顺序、空值、字符串编码、数字范围和数组语义。VMP 可保护业务字段白名单和域分隔,但通用序列化保持可测试。客户端和服务端共用固定公开向量,任何版本漂移都应产生明确 protocolVersion,而不是在失败时忽略 hash。
hash mismatch 是强失败信号:响应可能属于另一个请求、序列化不同或上下文被修改。客户端不得回落到缓存 verdict,服务端不得仅记录警告后继续。排查时保存脱敏输入摘要、策略版本、响应请求详情和候选版本,不记录真实令牌或用户敏感数据。
| 字段 | 纳入原因 | 失败条件 | 边界 |
|---|---|---|---|
| operation | 区分业务动作 | 未知动作 | 不代表授权 |
| resourceId | 绑定目标对象 | 服务器找不到 | 需对象级授权 |
| amountMinor | 绑定关键数值 | 类型或范围异常 | 服务端重算 |
| subjectHint | 防止交叉请求 | 与认证主体不符 | 不替代 token |
| nonce | 区分请求实例 | 重复或缺失 | 服务端消费 |
| protocolVersion | 固定字节规则 | 未知版本 | 拒绝静默兼容 |
按操作风险设计降级,而不是按设备标签一刀切
低风险公开读取可以在信号超时时继续,但应限制速率、数据范围和会话权限;可恢复写入可以进入待确认队列;资金、密钥、账号接管和高权限管理通常保持拒绝,等待完整证据与额外认证。风险分级来自业务影响,不来自设备型号或单一 verdict。
unsupported 不应自动等于恶意。某些合法环境可能无法提供期望信号,产品要提供人工审核、Web 流程、客服验证或受限模式等替代路径。但替代路径不能通过客户端自行声明 unsupported 获得,服务器必须根据平台响应、账号历史和操作上下文决定。
明确风险 verdict 也不能被客户端直接转成永久封禁。服务器组合账户、设备、请求和行为证据,选择拒绝当前操作、要求认证、限速或调查。公开文案只说明操作暂不可用,不暴露可以帮助枚举策略的内部阈值。
| 操作级别 | 信号超时 | 设备不支持 | 明确风险 |
|---|---|---|---|
| 公开内容 | 受限读取 | 受限读取 | 限速或拒绝 |
| 账号资料读取 | 短重试 | 替代验证 | 要求重新认证 |
| 可恢复草稿 | 本地保存待同步 | 只读或待审 | 拒绝提交 |
| 普通写入 | 延后 | 人工替代路径 | 拒绝 |
| 资金与购买 | 拒绝 | 服务端复核 | 拒绝并记录 |
| 权限与密钥 | 拒绝 | 强认证流程 | 拒绝并调查 |
缓存只服务短期可用性,不能把旧 verdict 变成身份
缓存键至少包含账户主体、操作类型、资源或请求摘要、应用版本、策略版本和平台响应类型。只按设备 ID 或时间保存 trusted=true 会把一次低风险结果复用到高风险操作,也可能跨账号、跨候选使用。缓存记录必须有明确 issuedAt 与 expiresAt。
缓存有效期由后端返回并受本地更短上限约束。系统时钟回拨、策略版本变化、账户切换、应用升级、请求摘要变化或服务器撤销都会使缓存失效。客户端无法确认年龄时回到 unknown,不得将解析失败当作仍在有效期。
高风险操作可以完全禁用 verdict 缓存,或只缓存平台准备状态而不缓存业务 allow。缓存命中后服务器仍验证主体与操作,客户端只是减少重复准备成本。报告必须区分 cached-platform-evidence 和 cached-business-decision,避免名字造成越权使用。
错误与重试要有预算、幂等和可观测边界
网络超时可有限重试,但重试次数、总时长和退避必须明确。对会产生副作用的业务操作,服务端使用幂等键,确保完整性请求重试不会重复扣款或创建对象。客户端超出预算后返回 retry-later 或 deny,不进入无限旋转,也不切换到未验证接口。
错误分类要保留平台错误、网络错误、解析错误、后端验证错误、请求绑定错误和业务拒绝。日志只记录枚举、耗时、策略版本和脱敏摘要,不保存原始 token、完整 verdict 或客户数据。这样既能排查不可用率,也避免诊断系统成为凭据泄露面。
熔断只影响请求调度,不能改变授权策略。平台服务异常时可减少重复调用并快速返回 limited,但资金和权限操作仍由服务器保持拒绝。恢复后先通过健康探针与小流量验证,再逐步打开,不把熔断结束自动解释为所有设备可信。
| 异常 | 可重试 | 缓存可用 | 默认动作 |
|---|---|---|---|
| 网络超时 | 有限次数 | 仅同上下文短期 | 按风险延后或拒绝 |
| 平台限流 | 按建议退避 | 不扩大范围 | limited |
| 解析错误 | 不重试同响应 | 不可用 | 拒绝 |
| 后端验证错误 | 修复后新请求 | 不可用 | 拒绝 |
| requestHash 不符 | 不可重试旧响应 | 不可用 | 拒绝并调查 |
| 业务 deny | 不通过 SDK 重试 | 不可绕过 | 保持拒绝 |
VMP 保护状态转移与失败关闭,不整体虚拟化 SDK
优先候选包括 integrityState 到 businessAction 的纯函数、缓存键构造、过期判断、重试预算扣减和禁止 unknown-to-allow 分支。这些方法输入有限、输出枚举明确,泄露后会降低篡改成本,同时可用笛卡尔积测试穷举全部组合。
SDK 客户端、网络、JSON 解析、加密库、线程调度和系统服务通常保持在外。整体虚拟化会增加异步回调、版本升级和设备兼容风险,却不能让本地结果变真。平台库通过官方版本、来源和集成测试治理,VMP 规则只覆盖项目特有决策。
保护后要检查新增枚举和异常是否落入安全默认值。Kotlin sealed class 或 enum 扩展时,测试应强制所有状态显式映射;捕获 RuntimeException 后不能 return ALLOW。调用图还要确认所有敏感入口都使用同一服务器裁决,不存在旧 API 或调试开关旁路。
用穷举脚本检查未知放行、无限缓存和缺少服务器裁决
下面的 Python 示例读取公开 policy.json,其中定义 signalStates、operationRisks 和 transitions。它要求每个状态与风险组合都有明确动作,拒绝 unknown、timeout、verification-error 或 hash-mismatch 映射为 allow,拒绝无限缓存,并确认敏感操作都标记 serverRequired。
代码不调用真实完整性 API,不含令牌、内部地址或客户数据。输入派生的缺失组合和危险动作会输出具体错误后非零退出,适合作为策略代码生成前的静态门禁。生产项目可把源码状态机投影为同一格式,防止文档与实现分离。
脚本通过只证明表格满足这些结构约束,不证明平台 verdict 已验证、VMP 配置生效或业务服务器执行相同策略。下游仍要用真实候选、测试账号、requestHash、后端回执和设备矩阵验证,每个结论绑定策略与候选摘要。
import itertools
import json
import sys
from pathlib import Path
if len(sys.argv) != 2:
raise SystemExit("usage: audit_integrity_fallback.py policy.json")
policy_path = Path(sys.argv[1]).resolve()
if not policy_path.is_file():
raise SystemExit(f"policy missing: {policy_path.name}")
with policy_path.open("r", encoding="utf-8") as stream:
policy = json.load(stream)
if not isinstance(policy, dict):
raise SystemExit("policy root must be an object")
states = policy.get("signalStates")
risks = policy.get("operationRisks")
transitions = policy.get("transitions")
if not all(isinstance(item, list) and item for item in (states, risks)):
raise SystemExit("signalStates and operationRisks must be non-empty arrays")
if not isinstance(transitions, dict):
raise SystemExit("transitions must be an object")
allowed_actions = {"deny", "retry", "limited", "read-only", "defer", "allow"}
unsafe_states = {"unknown", "timeout", "no-verdict", "verification-error", "hash-mismatch"}
errors = []
for state, risk in itertools.product(states, risks):
key = f"{state}:{risk}"
rule = transitions.get(key)
if not isinstance(rule, dict):
errors.append(f"missing transition {key}")
continue
action = rule.get("action")
if action not in allowed_actions:
errors.append(f"invalid action for {key}: {action}")
if state in unsafe_states and action == "allow":
errors.append(f"unsafe default allow for {key}")
cache_seconds = rule.get("cacheSeconds", 0)
if not isinstance(cache_seconds, int) or cache_seconds < 0 or cache_seconds > 300:
errors.append(f"invalid cache window for {key}")
if risk in {"financial", "privileged"} and rule.get("serverRequired") is not True:
errors.append(f"server decision missing for {key}")
if errors:
print(json.dumps({"status": "failed", "errors": errors}, indent=2))
raise SystemExit("integrity fallback policy has unsafe transitions")
print(json.dumps({
"status": "passed",
"checked_combinations": len(states) * len(risks),
"boundary": "structure passed; real verdict and server enforcement remain separate gates",
}, indent=2))发布证据要覆盖故障注入、服务端执行和实际候选
NIST SP 800-218 SSDF 要求保留来源、构建、验证和变更证据。对应完整性降级,证据包包含状态机版本、VMP 方法范围、平台 SDK 版本、requestHash 规则、缓存上限、重试预算、后端策略、故障注入结果、候选摘要和设备范围。
OWASP MASVS-RESILIENCE 将抗逆向与抗篡改视为纵深控制。保护规则和静态门禁不能证明攻击已被阻断,也不能证明 Root 状态。公开报告只陈述真实观察,不提供客户、性能、兼容、排名、收录或阻断数字,数据缺失就明确标记。
完整性策略若依赖远端配置,可查看[远端配置安全决策的 VMP 边界](/zh-cn/articles/remote-config-security-decision-vmp-boundary/),避免本篇重复回答配置签名问题。需要提交候选与状态机,可从[御盾中央平台申请加固服务](https://www.leonadev.com/console/)。登录、注册、价格、购买和控制台统一由中央平台承接。
- 超时、无 verdict、不支持、验证失败和明确风险使用不同状态
- requestHash 绑定受保护操作并由服务端重算
- unknown 与异常路径从不默认映射为 allow
- 缓存绑定主体、操作、请求摘要、策略版本和短有效期
- 金融、权限与密钥操作始终需要服务器裁决
- 重试有次数、总时长、退避和业务幂等边界
- VMP 只覆盖状态映射、缓存和失败关闭小方法
- 故障注入、真实后端与设备证据绑定同一候选
事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| 完整性信号应靠近受保护操作请求,并由后端解密、验证和决策。 | Play Integrity overview 说明完整性检查的请求位置与后端职责。 | 平台信号不证明 VMP 配置、Root 状态或攻击已被阻断。 |
| 标准请求可用 requestHash 绑定稳定序列化的业务请求并由服务端重算。 | Play Integrity standard requests 说明标准请求与 requestHash 使用方式。 | 完整性信号不是绝对可信根,最终允许仍由服务端结合业务决定。 |
| 完整性响应包含请求详情和多类平台信号,后端应按业务场景组合。 | Play Integrity verdicts 说明响应字段与 verdict 类别。 | 任何单一 verdict 都不是绝对信任、Root 判定或 VMP 证明。 |
| 服务端许可校验需要绑定用户、nonce、签名响应并防止重放。 | Server-side license verification 说明许可响应的服务端校验要点。 | 该流程不等于任意离线授权协议,也不定义完整性降级策略。 |
| 移动端抗逆向与抗篡改属于纵深防御,不能替代服务端授权。 | OWASP MASVS-RESILIENCE 定位移动端 resilience 控制。 | 控制目录不证明某个候选达到具体防护强度。 |
| 安全发布应保存来源、构建、验证、变更和供应链风险证据。 | NIST SP 800-218 SSDF 给出组织级安全软件开发实践。 | SSDF 不定义具体状态机,也不证明候选运行通过。 |
| 状态转移、缓存边界和失败关闭是优先 VMP 候选。 | 工程判断:这些小方法直接决定不可用时的业务动作并可穷举。 | 是否纳入仍需实际调用图、性能、异常和兼容证据。 |
| 平台 SDK、网络和解析库不应因位于同一链就整体 VMP。 | 工程判断:扩大平台耦合增加回归面,却不提升本地证据真实性。 | 项目出现具体旁路证据时仍需按方法重新评审。 |
| 敏感操作必须由服务器执行完整性与业务裁决。 | 项目证据尚未接入:这是上线门禁,不表示当前后端已经实现。 | 客户端缓存、界面和脚本通过都不能登记服务器执行完成。 |
| 本文不提供客户、性能、兼容、排名、收录或攻击阻断结论。 | 项目证据尚未接入:缺少真实候选、后端回执与设备矩阵。 | 文章只提供可复核范围与状态机门禁。 |
工程常见问题
完整性请求超时是否应该默认放行?
不应该。按操作风险选择有限重试、只读、延后或拒绝,敏感操作必须等待服务端裁决。
设备不支持完整性信号是否等于恶意设备?
不等于。可提供受限或人工替代路径,但资格由服务器判断,客户端不能自行声明后放行。
缓存上一次通过 verdict 能否解决平台故障?
只能在同主体、同操作、同请求摘要、同策略版本和短有效期内有限复用,高风险操作可完全禁用。
VMP 应该保护整个 Play Integrity SDK 吗?
通常不应。优先保护状态映射、缓存条件、重试预算和失败关闭,SDK 保持官方实现与可升级。
requestHash 相同是否表示业务请求可信?
不表示。它只帮助绑定响应与输入,服务端仍要验证主体、对象权限、nonce、数值和业务状态。
明确风险 verdict 是否应永久封禁账号?
不能由客户端直接决定。服务器结合账户、请求和行为证据选择拒绝、认证、限速或调查。
状态机脚本通过能否证明 VMP 生效?
不能。它只检查策略结构,真实候选规则、后端执行、requestHash 和设备故障注入仍需回执。
怎样防止新 SDK 状态意外默认放行?
使用显式枚举和穷举门禁,未知状态或异常统一落入拒绝或重新确认,而不是 catch 后返回 allow。