先看结论与判断条件
- 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 增加高价值调用规则的分析与修改成本,再由服务端验证主体、请求、权限和重放状态。
| 层级 | 主要职责 | 默认 VMP 决策 | 不能宣称 |
|---|---|---|---|
| 平台 Keystore | 保管密钥句柄和执行受限操作 | 不属于应用保护目标 | 业务授权已完成 |
| 平台兼容包装层 | 统一版本和异常接口 | 通常低优先级 | 接触密钥等于核心资产 |
| 密钥策略构造 | 设置用途、认证和算法参数 | 逐方法评估 | 参数正确无需设备验证 |
| 请求绑定层 | 选择并规范化业务字段 | 高价值候选 | 摘要等于服务端授权 |
| 权益与风险决策 | 判断是否允许高价值动作 | 优先候选 | 客户端可以最终可信 |
| 服务端验证 | 核对用户、nonce、权限与重放 | 不在客户端 VMP 范围 | 单一信号绝对可信 |
先沿调用图找到承载决策的手写方法
从 Keystore API 向上追踪调用图,可以看到平台包装器、密钥仓库、认证协调器、请求签名器、业务用例和界面入口。若从下往上把整条链全部保护,启动、高频加密和异常路径会迅速扩大。更有效的方法是给每层标记输入、输出、业务损失、执行频率、线程、故障回退和服务端复核,然后选择真正改变允许或拒绝结果的方法。
高价值调用方具有具体信号:它决定可签名字段集合,绑定账号和资源,校验一次性挑战,拒绝过期状态,限制操作类型,或把服务端授权结果转换为本地动作。单纯 loadKey、signBytes、decryptBlob 或 catch 平台异常的方法,默认作为兼容边界。若包装器包含定制策略,则按实际分支而不是类名升级优先级。
保护清单需要连接原始符号与最终产物。R8 mapping、DEX 清单、调用图和 VMP 命中报告必须来自同一 APK。Kotlin 挂起函数、默认参数、lambda 和编译器合成方法可能改变最终入口,不能只复制源码方法名。项目证据应说明业务资产如何映射到最终方法,以及未命中的合成入口是否仍能到达受保护核心。
| 评审字段 | 要回答的问题 | 高价值信号 | 降级信号 |
|---|---|---|---|
| 输入范围 | 哪些字段参与加密或签名 | 决定账号、资源和操作 | 只传 byte 数组 |
| 业务分支 | 什么条件允许调用 | 权益、风险、nonce 与有效期 | 无条件平台包装 |
| 服务端复核 | 服务端会重算什么 | 请求和主体可绑定 | 只信任客户端布尔值 |
| 执行频率 | 启动或请求中调用多少次 | 低频且边界清楚 | 高频循环和主线程 |
| 失败语义 | 失效、取消和异常如何处理 | 失败关闭且可测试 | 吞异常后继续 |
| 回滚能力 | 保护异常怎样隔离 | 配置分组和旧候选可用 | 改变即阻断全局启动 |
密钥策略参数与业务规则分别保护
密钥生成时的用途、算法、认证要求和有效条件会限制后续操作。参数构造方法值得审阅,但强度取决于其是否承载产品策略。若代码只是固定采用平台批准配置,它更像安全配置;若根据账号、功能或风险动态选择认证窗口、别名或操作集合,就可能成为敏感决策。VMP 保护的是选择规则,不是把 KeyGenParameterSpec 或密钥对象再次包一层。
别名本身通常不是秘密,却连接密钥生命周期。调用方需要防止测试与正式别名串用、账号切换后复用旧身份、应用数据恢复后指向不存在密钥。别名构造应使用稳定但不泄露敏感数据的标识,服务端不把别名当成用户证明。工程判断是将别名和密钥元数据纳入状态机测试,而不是依赖字符串隐藏。
失败策略比成功路径更关键。用户认证取消、密钥永久失效、设备锁变化、存储清除、算法不可用和平台异常,都可能进入恢复分支。业务代码必须区分需要重新认证、重新注册、回到服务端校验和彻底拒绝的情况。若保护后异常类型或传播路径改变,错误恢复可能被错误地当成成功,因此每类异常都要设备端回归。
| 方法类型 | 主要价值 | 保护建议 | 回归重点 |
|---|---|---|---|
| 固定参数构造 | 保持安全配置一致 | 低到中优先级 | 平台版本和生成成功 |
| 动态用途选择 | 控制可执行操作 | 高优先级候选 | 账号、功能与风险分支 |
| 别名映射 | 连接主体和密钥生命周期 | 逐项目评估 | 切号、恢复和清除 |
| 用户认证协调 | 决定认证后能否继续 | 高优先级候选 | 取消、超时和失效 |
| 平台异常包装 | 统一错误接口 | 通常兼容优先 | 错误类型与传播 |
| 业务降级决策 | 决定失败时是否放行 | 高优先级候选 | 失败关闭与服务端状态 |
请求绑定方法比签名调用本身更值得保护
签名函数可能只有几行:取得密钥、初始化 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
- 无例外包装层不会被整体保护
- 反射与异步入口补显式证据
- 未知调用目标默认阻断
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 组合已经兼容所有设备。
| 场景 | 关键断言 | 业务失败动作 | 证据 |
|---|---|---|---|
| 首次生成 | 参数和别名符合策略 | 停止高价值操作 | 设备、系统和候选摘要 |
| 认证取消 | 不会被当成认证成功 | 保留用户状态并拒绝 | 异常与界面结果 |
| 密钥失效 | 能识别并进入重注册 | 服务端撤销旧绑定 | 失效原因和新身份 |
| 应用重启 | 状态和服务端绑定一致 | 不复用过期请求 | 重启前后回执 |
| 并发调用 | 请求和签名不会串用 | 按业务规则序列化或拒绝 | 并发输入与结果 |
| 服务端拒绝 | 客户端不能本地改写放行 | 清理短期状态并提示 | 请求绑定和响应状态 |
发布证据连接调用规则、服务端验证和同一候选
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 和保护清单,再通过御盾中央平台提交申请。