先看结论与判断条件

  • Keystore API 包装层通常只负责生成、加载和调用密钥,不应因为接触密钥就自动成为最高强度 VMP 目标。
  • 优先评估决定密钥别名、用途、用户认证、请求摘要、授权范围、失败回退和服务端提交条件的手写业务方法。
  • 密钥不可导出不等于明文输入输出不可观察,也不等于调用方、设备或用户天然获得业务权限。
  • Play Integrity 等环境信号要与稳定序列化的业务请求绑定,并由服务端重算核对,客户端 VMP 不能代替该验证。
  • 许可、权益和高价值操作由服务端绑定用户、nonce、签名响应与重放状态,Keystore 签名只提供一项可验证材料。
  • 设备回归需覆盖首次生成、已有密钥、用户认证、失效、清除、升级、并发与异常,并绑定同一加固候选。

Keystore 保护密钥用途,不替调用方做业务决定

Android Keystore 可以让密钥材料保持不可导出,限制密钥用途,并在可用设备上由硬件安全能力支持。应用通过 KeyStore、KeyGenerator、KeyPairGenerator、Cipher、Signature 或 Mac 等接口请求操作。系统负责密钥句柄和加密运算边界,但不知道某个用户是否购买功能、某个请求是否属于当前会话、某次签名是否应该提交给服务端。

这一区分决定 VMP 重点。直接包装 Keystore API 的方法往往只做参数传递、异常转换和平台版本适配,结构稳定且业务价值有限。真正容易被修改后造成损失的逻辑,通常位于上层:选择哪个别名与用途,是否要求用户认证,哪些字段进入请求摘要,何时允许签名,失败时降级还是拒绝,以及服务端返回什么状态才继续业务。

密钥不可导出也不等于明文不可观察。数据进入 Cipher 或 Signature 前已经在应用进程中可用,解密后的内容还会交给业务函数;被控制的调用方可能请求 Keystore 对不应授权的数据执行操作。工程判断是用 Keystore 约束密钥,用 VMP 增加高价值调用规则的分析与修改成本,再由服务端验证主体、请求、权限和重放状态。

Keystore 与上层业务代码的职责
层级主要职责默认 VMP 决策不能宣称
平台 Keystore保管密钥句柄和执行受限操作不属于应用保护目标业务授权已完成
平台兼容包装层统一版本和异常接口通常低优先级接触密钥等于核心资产
密钥策略构造设置用途、认证和算法参数逐方法评估参数正确无需设备验证
请求绑定层选择并规范化业务字段高价值候选摘要等于服务端授权
权益与风险决策判断是否允许高价值动作优先候选客户端可以最终可信
服务端验证核对用户、nonce、权限与重放不在客户端 VMP 范围单一信号绝对可信

先沿调用图找到承载决策的手写方法

从 Keystore API 向上追踪调用图,可以看到平台包装器、密钥仓库、认证协调器、请求签名器、业务用例和界面入口。若从下往上把整条链全部保护,启动、高频加密和异常路径会迅速扩大。更有效的方法是给每层标记输入、输出、业务损失、执行频率、线程、故障回退和服务端复核,然后选择真正改变允许或拒绝结果的方法。

高价值调用方具有具体信号:它决定可签名字段集合,绑定账号和资源,校验一次性挑战,拒绝过期状态,限制操作类型,或把服务端授权结果转换为本地动作。单纯 loadKey、signBytes、decryptBlob 或 catch 平台异常的方法,默认作为兼容边界。若包装器包含定制策略,则按实际分支而不是类名升级优先级。

保护清单需要连接原始符号与最终产物。R8 mapping、DEX 清单、调用图和 VMP 命中报告必须来自同一 APK。Kotlin 挂起函数、默认参数、lambda 和编译器合成方法可能改变最终入口,不能只复制源码方法名。项目证据应说明业务资产如何映射到最终方法,以及未命中的合成入口是否仍能到达受保护核心。

Keystore 调用链的评审字段
评审字段要回答的问题高价值信号降级信号
输入范围哪些字段参与加密或签名决定账号、资源和操作只传 byte 数组
业务分支什么条件允许调用权益、风险、nonce 与有效期无条件平台包装
服务端复核服务端会重算什么请求和主体可绑定只信任客户端布尔值
执行频率启动或请求中调用多少次低频且边界清楚高频循环和主线程
失败语义失效、取消和异常如何处理失败关闭且可测试吞异常后继续
回滚能力保护异常怎样隔离配置分组和旧候选可用改变即阻断全局启动

密钥策略参数与业务规则分别保护

密钥生成时的用途、算法、认证要求和有效条件会限制后续操作。参数构造方法值得审阅,但强度取决于其是否承载产品策略。若代码只是固定采用平台批准配置,它更像安全配置;若根据账号、功能或风险动态选择认证窗口、别名或操作集合,就可能成为敏感决策。VMP 保护的是选择规则,不是把 KeyGenParameterSpec 或密钥对象再次包一层。

别名本身通常不是秘密,却连接密钥生命周期。调用方需要防止测试与正式别名串用、账号切换后复用旧身份、应用数据恢复后指向不存在密钥。别名构造应使用稳定但不泄露敏感数据的标识,服务端不把别名当成用户证明。工程判断是将别名和密钥元数据纳入状态机测试,而不是依赖字符串隐藏。

失败策略比成功路径更关键。用户认证取消、密钥永久失效、设备锁变化、存储清除、算法不可用和平台异常,都可能进入恢复分支。业务代码必须区分需要重新认证、重新注册、回到服务端校验和彻底拒绝的情况。若保护后异常类型或传播路径改变,错误恢复可能被错误地当成成功,因此每类异常都要设备端回归。

密钥策略相关方法的 VMP 选择
方法类型主要价值保护建议回归重点
固定参数构造保持安全配置一致低到中优先级平台版本和生成成功
动态用途选择控制可执行操作高优先级候选账号、功能与风险分支
别名映射连接主体和密钥生命周期逐项目评估切号、恢复和清除
用户认证协调决定认证后能否继续高优先级候选取消、超时和失效
平台异常包装统一错误接口通常兼容优先错误类型与传播
业务降级决策决定失败时是否放行高优先级候选失败关闭与服务端状态

请求绑定方法比签名调用本身更值得保护

签名函数可能只有几行:取得密钥、初始化 Signature、传入字节并返回结果。真正决定安全语义的是待签名字节如何生成。若请求摘要遗漏账号、资源、操作、金额、版本或 nonce,攻击者可能在其他上下文复用合法签名。VMP 应优先考虑规范化、字段允许列表、上下文绑定和过期判断方法,同时让服务端使用相同协议重建。

Play Integrity standard requests 提供 requestHash,用于把完整性响应绑定到稳定序列化后的业务请求,并由服务端重算核对。这个事实支持的边界是:客户端先稳定构造业务摘要,服务端收到原始业务字段后独立重算并比较。不能把 requestHash 当成秘密,也不能只在客户端验证后上传一个“可信”布尔值。

稳定序列化必须定义字段顺序、编码、缺省值、大小写、数值和重复字段处理,客户端与服务端共用测试向量。摘要输入不含原始令牌和不必要个人数据,日志只记录摘要和协议版本。工程判断是保护序列化和请求策略可以提高篡改成本,但服务端重算才建立最终绑定;两端协议漂移时应失败而不是退回弱校验。

请求绑定的最小字段与验证位置
字段客户端职责服务端职责遗漏风险
主体使用当前会话标识构造请求绑定服务端账号跨账号重放
资源包含模型、权益或对象标识检查所有权和范围跨资源复用
操作区分签名、解密或授权动作只允许批准操作用途混淆
nonce使用服务端一次性挑战核对并标记已使用重复提交
版本声明协议和应用候选选择匹配解析规则两端序列化漂移
请求摘要按稳定规则计算从原始字段独立重算只信客户端结果

许可和权益最终由服务端验证

Server-side license verification 描述服务端许可校验需要绑定用户、nonce、签名响应并防止重放。虽然具体流程不等于任意离线授权协议,但边界清楚:客户端可以采集和转发材料,不能独自宣布许可永久有效。服务端验证响应签名、用户与应用关系、nonce、时间和历史状态,再返回短期、限权的业务决定。

Keystore 私钥可以为请求提供安装实例相关签名,但公钥注册、账号绑定、轮换和撤销仍由服务端维护。设备恢复或重装后产生新密钥时,不能自动继承旧账号高权限;服务端要经过合法会话和风险检查更新登记。VMP 可保护注册与调用流程中的本地规则,却不能把未经登记的公钥变成可信身份。

离线需求需要单独威胁模型。某些功能必须在短时离线工作,可以缓存服务端签发、带范围和过期的授权材料,并由 Keystore 保护本地状态或签名;但任何离线决定都存在时钟、回滚和被控制客户端的边界。本文不定义通用离线协议,也不宣称 VMP 能消除该风险。

  • 服务端绑定用户、应用、资源和 nonce
  • 签名响应在服务端验证而非客户端自证
  • 重放状态与业务操作同一事务处理
  • 公钥注册和轮换经过合法会话
  • 离线授权有明确范围、过期与恢复策略
  • 客户端拒绝结果不能被本地布尔值覆盖

用调用图清单定位真正承载业务决策的方法

自动门禁可以读取构建阶段导出的调用图清单。每个方法记录稳定资产标识、最终符号、所属层、直接调用目标、是否含业务决策和预期保护。Keystore API 节点标为 platform,包装层标为 wrapper,授权、请求绑定和风险逻辑标为 business。验证器从每个业务资产沿调用边确认最终到达 Keystore 操作,同时检查包装层没有因为名称命中被整体保护。

下面 Python 示例只读取公开结构 JSON,不解析客户字节码,也不输出真实类名。它要求方法标识唯一、调用目标存在、至少有一个平台节点;从每个 protect-business 方法做有限图遍历,必须能够到达 Keystore 节点;实际保护集合不能包含无例外 wrapper,预期业务方法也不能漏保护。真实流水线还要将清单、mapping 和保护报告绑定同一 APK 摘要。

图可达不等于运行时一定执行,也不证明业务规则正确。反射、JNI、回调和异步状态机可能让静态边缺失,项目需要为这些入口补显式关系或设备证据。工程判断是把无法解释的未知调用标为阻塞,不用模糊包名扩大保护来掩盖清单缺口。

  • 调用图和保护报告来自同一候选
  • 平台、包装和业务层使用稳定资产标识
  • 业务保护目标确实能到达 Keystore
  • 无例外包装层不会被整体保护
  • 反射与异步入口补显式证据
  • 未知调用目标默认阻断
检查 Keystore 调用图中的业务保护目标
from pathlib import Path
import json
import sys

if len(sys.argv) != 2:
    raise SystemExit("usage: check_keystore_callers.py call-graph.json")

source = Path(sys.argv[1])
if not source.is_file():
    raise SystemExit("call graph file does not exist")

payload = json.loads(source.read_text(encoding="utf-8"))
methods = payload.get("methods")
protected = set(payload.get("protectedMethods", []))
if not isinstance(methods, list) or not methods:
    raise SystemExit("method inventory is required")

by_id = {}
failures = []
for method in methods:
    method_id = method.get("assetId")
    if not method_id or method_id in by_id:
        failures.append(f"invalid or duplicate method: {method_id!r}")
        continue
    by_id[method_id] = method

platform_ids = {key for key, value in by_id.items() if value.get("layer") == "platform-keystore"}
if not platform_ids:
    failures.append("no Keystore platform method is present")

for method_id, method in by_id.items():
    calls = method.get("calls", [])
    unknown = [target for target in calls if target not in by_id]
    if unknown:
        failures.append(f"{method_id}: unknown call targets {unknown!r}")
    decision = method.get("protectionDecision")
    if decision == "protect-business":
        pending = list(calls)
        visited = set()
        reaches_keystore = False
        while pending:
            target = pending.pop()
            if target in visited or target not in by_id:
                continue
            visited.add(target)
            if target in platform_ids:
                reaches_keystore = True
                break
            pending.extend(by_id[target].get("calls", []))
        if not reaches_keystore:
            failures.append(f"{method_id}: business method does not reach Keystore")
        if method_id not in protected:
            failures.append(f"{method_id}: expected business method was not protected")
    if method.get("layer") == "wrapper" and method_id in protected and not method.get("exceptionReason"):
        failures.append(f"{method_id}: wrapper protected without an exception")

if failures:
    print("Keystore caller boundary gate failed:")
    for failure in failures:
        print(f"- {failure}")
    raise SystemExit(2)

print(f"validated {len(by_id)} method records")

设备回归覆盖密钥状态和受保护异常路径

Android instrumented tests 适合验证依赖真实 Android 运行时、组件、锁屏和系统 API 的行为。Keystore 测试不能只在 JVM 模拟里断言接口返回值,应在目标设备覆盖首次生成、已有密钥读取、用户认证成功与取消、应用重启、系统升级、密钥失效、数据清除、并发调用和异常恢复。

VMP 可能改变调用方的执行时延、异常栈和线程行为。高频签名、启动时解密和主线程认证协调需要先建立未保护基线,再用同一设备和候选比较。项目应分别记录 Keystore 操作耗时、上层序列化、服务端往返和业务完成时间,不能把网络与硬件差异全部归因于保护。

硬件保护能力和系统行为随设备而异,单一设备通过不能代表完整 API、ABI 和厂商矩阵。回归报告明确哪些设备支持何种安全级别、哪些异常实际触发、哪些组合未执行。没有真实设备证据时,只能说明方法和边界,不能写成 Keystore 与 VMP 组合已经兼容所有设备。

Keystore 调用方的最小设备回归
场景关键断言业务失败动作证据
首次生成参数和别名符合策略停止高价值操作设备、系统和候选摘要
认证取消不会被当成认证成功保留用户状态并拒绝异常与界面结果
密钥失效能识别并进入重注册服务端撤销旧绑定失效原因和新身份
应用重启状态和服务端绑定一致不复用过期请求重启前后回执
并发调用请求和签名不会串用按业务规则序列化或拒绝并发输入与结果
服务端拒绝客户端不能本地改写放行清理短期状态并提示请求绑定和响应状态

发布证据连接调用规则、服务端验证和同一候选

NIST SP 800-218 SSDF 将来源、构建、验证、变更和供应链风险纳入安全开发实践。Keystore 调用方的证据包应包含 APK 摘要、变体、密钥策略清单、调用图、R8 mapping、VMP 配置、实际命中报告、请求绑定协议版本、服务端校验规则和设备回归。秘密、私钥和完整用户数据不能进入证据包。

变更需要按边界触发复审。密钥参数、别名状态机、请求字段、nonce、服务端授权、完整性信号、保护范围或目标系统变化,都可能使旧结论失效。流水线比较清单和协议摘要,要求负责人解释扩大或降级。没有服务端回执、设备数据或同候选报告时,结论标为未接入或待验证。

最终结论应限定为指定候选中列出的业务调用方法进入保护,Keystore 包装层按策略处理,请求绑定和服务端验证在列出的场景完成;不能声称密钥绝对不可获取或客户端成为可信执行环境。准备评估时,可通过御盾中央平台提交代表性 APK、Keystore 调用图、密钥策略、请求协议和保护范围,先固定候选身份与验收边界。

  • 调用图、mapping 和保护报告绑定同一 APK
  • 密钥策略和请求协议都有版本摘要
  • 服务端验证规则与客户端字段一一对应
  • 设备回归覆盖真实失效与拒绝路径
  • 证据包不包含私钥和用户敏感明文
  • 结论区分 Keystore、VMP 与服务端责任

事实依据与适用边界

以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。

本文判断事实或工程依据适用限制
Android Keystore 可以限制密钥用途,并在可用设备上采用硬件保护。Android Keystore 描述不可导出密钥材料、用途限制和硬件安全能力。Keystore 不保护应用使用时的明文输入输出,也不替调用方完成业务授权。
抗逆向与抗篡改属于移动端纵深防御,不能替代服务端授权。OWASP MASVS-RESILIENCE 将相关要求放在移动应用韧性控制范围。控制目录不证明某个候选包达到特定强度,也不让客户端成为绝对可信环境。
标准完整性请求可以用 requestHash 绑定稳定序列化的业务请求并由服务端重算。Play Integrity standard requests 描述 requestHash 的请求绑定和服务端核对路径。完整性信号不是绝对可信根,最终允许或拒绝仍由服务端结合业务上下文决定。
服务端许可校验需要绑定用户、nonce、签名响应并处理重放。Server-side license verification 描述服务端许可响应验证与相关绑定。该流程不等于任意离线授权协议,也不证明所有本地缓存安全。
依赖真实 Android 运行时、组件和系统 API 的语义适合通过设备端测试验证。Android instrumented tests 描述在设备或模拟器上访问 Android 框架能力的测试。单一设备通过不能代表完整 API、ABI、硬件安全级别和厂商矩阵。
安全发布应保留来源、构建、验证和变更证据并管理供应链风险。NIST SP 800-218 SSDF 提供组织级安全软件开发与发布实践框架。SSDF 不定义具体 Keystore 调用方或 VMP 产品功能,也不证明项目已通过。
VMP 应优先评估决定密钥调用条件和请求绑定的业务方法,而不是密钥对象。工程判断:平台密钥边界、包装调用和业务授权的攻击收益与故障半径不同。具体方法仍需调用图、候选命中报告、性能基线和设备回归确认。
Keystore 签名、完整性信号和许可材料都需要服务端绑定具体主体与请求。工程判断:客户端材料只有在服务端重算、授权和重放状态中才能形成最终业务决定。协议字段、时效、离线边界和风险策略必须由真实项目证据确定。

工程常见问题

Keystore 里的密钥还需要 VMP 保护吗?

VMP 不直接保护密钥对象。Keystore 负责密钥用途和导出边界,VMP 更适合保护决定何时调用、绑定哪些业务字段和怎样处理失败的手写规则。

把 Keystore 包装类全部加入 VMP 是否更安全?

不一定。包装类多为平台适配和参数传递,全量处理会扩大性能与兼容风险。应从高价值业务决策向下追踪调用链。

密钥不可导出是否意味着签名请求不能被滥用?

不意味着。被控制的调用方仍可能请求 Keystore 对错误数据签名。服务端必须核对主体、资源、操作、nonce、权限和重放状态。

Play Integrity 的 requestHash 能否替代服务端授权?

不能。它帮助把完整性响应绑定到业务请求,服务端仍要重算摘要,并独立验证账号、资源、权限和风险。

一个真实设备测试通过是否代表所有 Keystore 设备兼容?

不能。系统版本、硬件安全能力、厂商实现和认证状态不同,需要目标矩阵逐项记录,未覆盖组合明确标记。

申请 Keystore 调用方 VMP 评估需要准备什么?

准备代表性 APK、调用图、密钥策略、请求绑定协议、服务端验证规则、R8 mapping 和保护清单,再通过御盾中央平台提交申请。

想用自己的 App 验证?

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

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