先看结论与判断条件
- 先冻结签名协议,再决定 VMP 范围;字段集合、规范化算法和验证顺序不能由保护工具隐式改变。
- 最值得保护的是业务字段到签名输入的映射、密钥调用编排和关键失败分支,而不是通用 JSON 库或 HTTP 客户端。
- RFC 8785 解决确定性 JSON 表示,不提供认证、授权、时效、设备可信或重放防护。
- Android Keystore 可以限制密钥用途并在部分设备上使用硬件保护,但不能让客户端输入自动成为可信事实。
- 请求唯一性、时间窗口、密钥绑定和服务端重算必须独立成立,VMP 不能代替这些服务端控制。
- 发布门禁应使用固定测试向量和服务端同源规则验证字节级一致性,并把性能、兼容与安全结论分别记录。
先定边界:保护签名决策,不把整条网络链虚拟化
请求签名通常跨越业务对象、字段筛选、JSON 或查询串规范化、摘要计算、密钥调用、请求头组装、网络发送和服务端校验。适合进入 VMP 的不是所有步骤,而是泄露后会明显降低伪造门槛、同时又具有稳定输入输出和有限平台依赖的客户端决策。把边界压到这几个方法,才便于验证语义与控制性能。
优先候选通常包括业务字段白名单、签名输入域分隔、版本号拼接、受保护密钥句柄调用和关键失败路径。通用序列化器、HTTP 栈、证书验证、日志、重试与响应解析不宜仅因属于同一调用链而纳入。它们分支多、依赖系统组件,扩大范围会增加兼容回归,却不会改变服务端是否接受请求。
最终信任必须落在服务端。客户端中的时间、nonce、设备标识、订单金额和权限声明都可能被操纵,服务端需要从自身状态重建可接受字段并执行授权。VMP 的价值是提高定位和修改签名编排的成本,不是把客户端声称的值升级为服务器事实,也不能承诺阻断所有逆向或重放。
| 组件 | 默认建议 | 主要理由 | 仍需验证 |
|---|---|---|---|
| 业务字段选择 | 优先纳入 | 暴露即说明签名覆盖边界 | 字段版本与兼容 |
| 签名输入组装 | 优先纳入 | 承载域分隔与顺序语义 | 字节级测试向量 |
| Keystore 调用编排 | 按需纳入 | 隐藏别名选择与错误分支 | 设备与提供者差异 |
| 通用 JSON 库 | 通常不纳入 | 成熟库且需独立复用 | 固定版本与输出 |
| HTTP 客户端 | 通常不纳入 | 平台耦合和分支较多 | 头字段传输一致 |
| 服务端重算 | 必须保留服务端 | 属于真实信任边界 | 授权与重放状态 |
威胁模型要区分协议泄露、密钥使用与服务端授权
先写出攻击者已经能观察什么。移动客户端分发到用户设备,字段名、接口路径、请求时序和公开算法不能假设永久保密。若协议安全性依赖攻击者不知道 JSON 排序或摘要算法,设计本身就缺少稳固边界。VMP 可以增加恢复具体实现和修改控制流的工作量,但不能成为唯一认证条件。
密钥风险要分为导出和滥用。Android Keystore 在可用设备上可以让密钥材料保持不可导出,并限制算法、用途或用户认证条件;然而攻击者仍可能尝试驱动应用调用密钥。服务端因此还需限制凭据权限、请求频率、有效时间、nonce 使用和账户状态,不能只验证签名字节正确。
授权风险与签名风险也要分开。有效签名最多说明请求由持有相应密钥的一方产生,不能证明订单属于该账号、资源可被当前角色访问或金额来自服务器报价。服务端要根据认证主体和数据库状态执行对象级授权,并把客户端字段视为待验证输入。范围评审若不写清这一点,很容易把加固强度误写成业务可信。
| 风险 | 客户端控制 | 服务端控制 | VMP 能做什么 |
|---|---|---|---|
| 协议被阅读 | 最小化敏感决策 | 不依赖算法保密 | 增加恢复实现成本 |
| 密钥被导出 | Keystore 与用途限制 | 轮换和撤销 | 保护调用编排 |
| 密钥被滥用 | 用户认证条件 | 限速与异常检测 | 增加劫持调用成本 |
| 请求重放 | nonce 与时间字段 | 唯一性存储和窗口 | 不能单独阻止 |
| 字段被篡改 | 本地约束和签名 | 重算与业务校验 | 保护字段映射 |
| 越权访问 | 不保存高权限事实 | 对象级授权 | 不能替代授权 |
规范化必须先冻结字节规则,再讨论保护范围
签名验证比较的是字节语义,不是开发者眼中的对象相等。属性顺序、Unicode 字符、转义、数字表示、空值、数组顺序和编码都可能改变输入。RFC 8785 JSON Canonicalization Scheme 给出确定性 JSON 表示规则,适合需要跨实现重算的协议;它只解决表示一致性,不产生认证或授权。
如果现有协议没有完整采用 JCS,也必须定义可执行的子集。例如仅允许字符串、布尔值、整数、数组和字符串键对象,明确空值是保留、删除还是拒绝,并拒绝浮点数进入金额或计数。客户端和服务端必须共享同一版本规则及测试向量,不能分别写一套看起来相似的排序函数。
规范化函数适合保持透明和可测试,未必值得进入 VMP。更好的边界常是把已规范化字节交给受保护的输入组装方法,由该方法添加协议版本、接口标识、HTTP 方法、主体摘要、时间和 nonce。这样规范化可以用公开向量独立验证,敏感的业务覆盖关系仍位于较小的保护单元。
| 对象 | 必须固定 | 失败关闭条件 | 测试证据 |
|---|---|---|---|
| 对象键 | 排序和编码 | 非字符串键 | 乱序输入同摘要 |
| 字符串 | UTF-8 与转义 | 非法 Unicode | 多语言向量 |
| 数字 | 允许类型和范围 | NaN、浮点歧义 | 边界值向量 |
| 空值 | 保留、删除或拒绝 | 双方规则不同 | 显式 null 用例 |
| 数组 | 顺序是否有语义 | 私自排序 | 顺序变化用例 |
| 未知字段 | 拒绝或版本化 | 静默进入签名 | 前后兼容用例 |
用数据流切片确定真正值得保护的方法
范围评审应从服务端验签输入向前回溯,而不是从包名或类名批量匹配。先标出服务器实际重算的字段、固定常量和上下文,再找到客户端中最后一次产生这些值的纯函数。只有直接影响签名字节、密钥选择或失败策略的方法进入候选;DTO getter、界面格式化和网络适配层保持在外。
候选方法最好满足输入封闭、输出明确、异常可枚举和平台调用少。若一个方法同时读取系统时间、随机数、账户缓存、设备属性、网络状态和 Keystore,虚拟化后的定位会非常困难。应先重构为 nonce 获取、规范化、签名输入组装和签名执行四个单元,再只保护其中包含业务秘密或关键控制流的部分。
还要审查调用者是否能绕开被保护方法。若另一路径可以直接构造请求头、复用旧签名或把未签名请求发送到兼容接口,保护一个核心方法没有覆盖真实链路。发布前需要静态调用图、运行时审计日志和服务端拒绝用例共同证明所有受保护操作都经过同一验签策略,但这些证据仍不能外推为不可绕过。
Keystore 负责密钥边界,VMP 负责调用编排边界
Android Keystore 允许应用生成或导入密钥并限制用途,在设备支持时可由硬件安全环境保护密钥材料。架构上应让签名私钥或对称密钥句柄停留在 Keystore API,不把原始密钥复制进 VMP 字节码、常量表、资源或配置。VMP 保护的是选择哪个别名、何时调用和如何处理失败,不是替代密钥容器。
硬件保护能力因设备、系统版本、密钥参数和提供者而异,不能从 API 调用成功推导所有设备都使用同一级别硬件。证据应记录生成参数、KeyInfo 或平台可提供的安全级别、用户认证要求、异常类型和测试设备范围。缺少真实候选与设备回执时,只能写设计要求,不能宣称密钥已不可导出。
密钥轮换需要协议字段支持。请求中应携带非敏感 key identifier 或版本,服务端维护启用、过渡和撤销状态;客户端选择逻辑可以进入 VMP,但服务端不能接受未知版本回退到旧密钥。出现 KeyPermanentlyInvalidatedException、认证超时或别名缺失时应显式失败,不要改用硬编码备用密钥。
重放控制必须由服务端持有状态并绑定请求上下文
签名正确的旧请求仍可能被重放。服务端应验证时间窗口、nonce 唯一性、HTTP 方法、目标 URI、主体摘要、认证主体和协议版本,并以原子方式消费一次性标识。客户端生成 nonce 只是输入来源,只有服务器确认从未使用并登记消费,才能形成重放控制。
RFC 9449 的 DPoP 展示了把访问令牌绑定到客户端持有密钥并用证明携带方法、URI、时间和唯一标识的标准化思路。它可以降低被盗 bearer token 的重放风险,但不能证明设备没有被控制,也不能替代短有效期、服务器授权、密钥撤销和错误响应治理。项目若不采用 DPoP,仍可借鉴其上下文绑定原则。
RFC 9700 汇总 OAuth 部署中的重定向、授权码注入、令牌重放和不安全 grant 风险。请求签名属于纵深控制,不应自行发明一套替代 OAuth 的长期凭据。范围设计必须说明它保护哪个资源、与 access token 如何绑定、服务器在哪一步验证,以及失败后是否泄露可供枚举的细节。
| 顺序 | 检查 | 失败处理 | 不能由客户端决定 |
|---|---|---|---|
| 1 | 协议与密钥版本 | 拒绝未知或撤销版本 | 允许的版本集合 |
| 2 | 认证主体与令牌 | 统一认证错误 | 账户有效状态 |
| 3 | 时间窗口 | 拒绝过期或未来偏差 | 服务器当前时间 |
| 4 | nonce 唯一性 | 原子拒绝已消费值 | 历史状态 |
| 5 | 规范化与签名 | 按固定规则重算 | 比较策略 |
| 6 | 对象级授权 | 拒绝越权资源 | 资源所有权 |
性能与兼容评审要测保护边界,不用猜测替代数据
请求签名位于用户操作或后台同步的热路径,VMP 范围扩大可能影响启动、首次调用、并发、异常路径和低端设备延迟。评审不能只测平均签名耗时,应分别观察规范化、受保护输入组装、Keystore 调用和网络等待,并比较同一候选、同一设备、同一负载的分位数据。
优化顺序是先缩小保护单元和减少跨边界对象,再讨论缓存。规范化结果只有在业务对象完全不可变且 nonce、时间、主体与接口上下文另行绑定时才可能复用;签名值通常不能跨请求缓存。为了性能复用旧 nonce 或去掉 URI 绑定会直接削弱重放防护,不属于可接受优化。
没有项目基准时文章不提供固定性能数字,也不声称某种范围必然无感。发布证据至少包含候选摘要、构建配置、设备与系统范围、调用次数、冷启动和稳态测量方法、错误率及回滚阈值。任何超过阈值的范围变化都应回到方法切片,而不是放宽服务端校验。
用公开测试向量验证规范化与服务端重算输入
下面的 Python 示例只验证一个明确受限的 JSON 子集:字符串键对象、字符串、布尔值、整数、空值和数组。它拒绝浮点数、非字符串键与未列入的字段,再用固定 UTF-8、键排序和紧凑分隔生成诊断字节。客户端样例和服务端样例顺序不同,但规范化摘要必须一致。
这段代码不是完整 JCS 实现,也没有私钥、令牌、内部地址或可运行攻击链。它故意只计算 SHA-256 诊断摘要,用于确认双方送入真正签名 API 的字节一致;生产认证必须使用经过评审的签名或 MAC 算法、Keystore 密钥和恒定时间比较,并由服务器执行授权与 nonce 消费。
门禁应把每个测试向量的原始对象、规范化字节、摘要、协议版本和实现版本保存下来。字段新增、null 规则、数值类型或 Unicode 处理一旦变化,都必须形成新协议版本并同时更新两端。VMP 后再次运行同一向量,结果不同就阻止发布,而不是调整服务端去兼容未知字节。
import hashlib
import json
import sys
from pathlib import Path
from typing import Any
ALLOWED_FIELDS = {"account_id", "action", "amount_minor", "items", "nonce", "timestamp"}
def validate_value(value: Any, path: str = "$") -> None:
if value is None or isinstance(value, (str, bool, int)):
return
if isinstance(value, float):
raise TypeError(f"floating point is forbidden at {path}")
if isinstance(value, list):
for index, item in enumerate(value):
validate_value(item, f"{path}[{index}]")
return
if isinstance(value, dict):
for key, item in value.items():
if not isinstance(key, str):
raise TypeError(f"non-string key at {path}")
validate_value(item, f"{path}.{key}")
return
raise TypeError(f"unsupported type at {path}: {type(value).__name__}")
def canonical_subset(payload: dict[str, Any]) -> bytes:
unknown = sorted(set(payload) - ALLOWED_FIELDS)
missing = sorted({"account_id", "action", "nonce", "timestamp"} - set(payload))
if unknown or missing:
raise ValueError(f"unknown={unknown}, missing={missing}")
validate_value(payload)
text = json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
return text.encode("utf-8")
if len(sys.argv) != 2:
raise SystemExit("usage: check_request_input.py public-vector.json")
input_path = Path(sys.argv[1]).resolve()
if not input_path.is_file():
raise SystemExit(f"test vector missing: {input_path.name}")
with input_path.open("r", encoding="utf-8") as stream:
client_payload = json.load(stream)
if not isinstance(client_payload, dict):
raise SystemExit("test vector root must be a JSON object")
server_reconstructed = dict(reversed(list(client_payload.items())))
client_bytes = canonical_subset(client_payload)
server_bytes = canonical_subset(server_reconstructed)
if client_bytes != server_bytes:
raise SystemExit("canonical request inputs differ")
print(json.dumps({
"canonical_utf8": client_bytes.decode("utf-8"),
"diagnostic_sha256": hashlib.sha256(client_bytes).hexdigest(),
"next_gate": "sign with the reviewed Keystore key and verify on the server",
}, ensure_ascii=False, indent=2))发布门禁要把协议、候选、证据和行动入口绑定起来
发布材料应绑定同一候选摘要、协议版本、VMP 规则摘要、客户端测试向量、服务端重算回执、Keystore 参数、目标系统范围和性能基线。若候选重新构建、重新签名或改变保护规则,旧回执不能沿用。OWASP MASVS-RESILIENCE 将抗逆向与抗篡改视为纵深控制,这与服务端授权和完整发布链是互补关系。
NIST SP 800-218 SSDF 强调把来源、构建、验证和变更证据纳入安全开发流程。对应请求签名,应保留规范化实现来源、依赖版本、字段变更审查、密钥轮换记录、服务端部署版本与回滚条件。组织级实践不能证明某个候选达到具体防护强度,因此公开结论必须停留在真实回执范围。
准备范围评审时,可先查看[数据访问业务规则的 VMP 边界](/zh-cn/articles/vmp-scope-for-data-access-business-rules/),区分客户端决策与服务端事实。需要提交候选和签名协议,可从[御盾中央平台申请加固服务](https://www.leonadev.com/console/)。登录、注册、价格、购买与控制台操作均由中央平台承接,提交材料前应移除生产密钥和真实用户数据。
- 签名协议字段、规范化规则和版本号已冻结
- VMP 只覆盖签名输入映射、密钥编排与关键失败分支
- 通用 JSON、HTTP 和服务端校验没有被错误并入客户端保护边界
- Keystore 中不导出原始密钥,异常路径不存在硬编码降级密钥
- 服务端按自身状态重建字段并执行 nonce、时间与对象级授权
- 客户端与服务端固定测试向量得到相同规范化字节
- 候选、规则、测试、设备与服务端部署证据可按摘要复算
- 性能和兼容结论只覆盖真实测试矩阵,不从静态门禁外推
事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| JSON 签名输入需要确定性的属性排序、字符串和数字表示,才能跨实现重算相同字节。 | RFC 8785 JSON Canonicalization Scheme 规定 JSON 确定性表示方法。 | JCS 只处理表示,不提供认证、授权、重放控制或设备完整性判断。 |
| DPoP 可以把访问令牌绑定到客户端持有的密钥并携带请求上下文证明。 | RFC 9449 OAuth DPoP 说明 proof、方法、URI、时间与唯一标识等机制。 | DPoP 不证明设备可信,也不替代短有效期、服务器授权和密钥撤销。 |
| Android Keystore 可限制密钥用途,并在设备支持时使用硬件保护密钥材料。 | Android Keystore 说明密钥存储、用途限制、用户认证与硬件支持边界。 | API 调用成功不证明所有设备具有相同硬件级别,也不保护业务明文。 |
| OAuth 部署应处理重定向、授权码注入、令牌重放和不安全 grant 风险。 | RFC 9700 OAuth Security BCP 汇总当前 OAuth 安全建议与已知攻击类别。 | 该 BCP 不定义项目的请求签名字段,也不能替代具体资源授权模型。 |
| 移动端抗逆向与抗篡改属于纵深防御,不能替代服务端授权和发布完整性。 | OWASP MASVS-RESILIENCE 将 resilience 控制定位于抗逆向与抗篡改能力。 | 控制目录不证明某个候选已达到防护强度,也不提供攻击阻断结论。 |
| 安全发布需要保留来源、构建、验证与变更证据并管理供应链风险。 | NIST SP 800-218 SSDF 给出组织级安全软件开发与发布实践。 | SSDF 不决定具体 VMP 方法范围,也不证明某次发布已通过。 |
| 业务字段映射、签名输入域分隔与密钥调用编排是优先 VMP 候选。 | 工程判断:这些方法直接决定服务端重算字节和密钥使用路径,且可切成有限输入输出单元。 | 是否纳入仍需候选包的调用图、性能、异常和兼容回执确认。 |
| 通用 JSON 库与 HTTP 客户端通常不应因位于签名链就整体进入 VMP。 | 工程判断:平台依赖与复杂分支会扩大回归面,而协议安全不应依赖通用算法保密。 | 若项目有特殊篡改路径,仍需基于真实调用证据单独评审,不能机械排除。 |
| 服务端必须原子消费 nonce,并按自身状态执行对象级授权。 | 项目证据尚未接入:这是发布前必须验证的服务器控制,不代表当前系统已经实现。 | 客户端时间、nonce 和签名正确都不能单独证明请求未重放或具备权限。 |
| 本文不提供性能提升、攻击阻断、客户案例、排名或收录结论。 | 项目证据尚未接入:缺少同一候选、设备矩阵、服务端回执和公开运营数据。 | 文章只给出可复核设计与门禁,不能作为具体产品效果证明。 |
工程常见问题
请求签名链是否应该全部放进 VMP?
不应该。优先保护业务字段映射、签名输入组装、密钥调用编排和关键失败分支,通用序列化、HTTP 与服务端校验保持独立。
JSON 按键排序后就等于采用 RFC 8785 吗?
不等于。JCS 还涉及数字、字符串和序列化规则;只实现子集时必须明确类型边界并拒绝未定义输入。
VMP 能防止签名请求被重放吗?
不能单独做到。服务器必须验证时间窗口、上下文并原子消费 nonce,客户端保护只能增加修改生成逻辑的成本。
密钥已经放入 Android Keystore,还需要 VMP 吗?
两者职责不同。Keystore 保护密钥材料和用途,VMP 可保护别名选择、输入组装与失败编排,但服务端仍需限权、轮换和撤销。
签名正确是否说明请求字段可信?
不说明。签名只绑定给定字节与密钥持有者,订单归属、金额、资源权限和账户状态仍要由服务器重算与授权。
规范化函数本身要进入 VMP 吗?
通常应保持透明可测试,把敏感字段映射和域分隔放入较小保护方法;若有特殊威胁证据,再单独评审规范化实现。
示例 SHA-256 摘要可以作为生产请求认证吗?
不可以。示例摘要只诊断双方字节是否一致,生产必须使用评审过的签名或 MAC、受保护密钥和服务器验证。
怎样判断保护范围没有拖慢请求?
对同一候选、设备和负载分别测规范化、受保护组装、Keystore 与整体请求的冷启动和稳态分位数据,再按预设阈值决策。