先看结论与判断条件
- Manifest intent filter、assetlinks.json 和系统域名验证属于平台信任链,VMP 不能修复主机、路径、签名指纹或用户链接策略错误。
- 原生应用是 OAuth 公共客户端,不应把静态 client secret 藏进 VMP;外部 user-agent、PKCE、严格重定向和授权服务器校验仍是协议基础。
- 回调处理要先执行重复参数拒绝、scheme、host、path、state、issuer 与错误分支校验,再把最小化的已验证结果交给业务策略层。
- 更适合 VMP 的对象是状态一次性消费、业务会话绑定、账户切换决定和高价值权益映射,而不是 Activity 生命周期、Uri 解析器或第三方 OAuth 库。
- 服务端负责最终业务 scope、会话、令牌权限和撤销;客户端 VMP 不能把公共客户端变成可信服务器,也不能让本地判断成为最终授权。
- 发布门禁要分别验证 App Link 关联、异常 URL、OAuth 负向用例、冷启动与返回栈,再对选定 VMP 函数做保护前后等价回归。
先画协议责任图,再选择值得保护的代码
App Link 登录从点击 HTTPS URL 开始,依次经过域名与应用关联、系统路由、Activity 或导航入口、URL 解析、OAuth 回调校验、授权码交换、业务会话建立和权益加载。每一层的信任来源不同。把整个回调函数做 VMP 只能改变部分客户端代码的可分析形态,不会改变 assetlinks.json、系统验证状态、授权服务器规则或服务端权限。
选择边界的核心问题不是哪段代码最靠近登录按钮,而是哪段代码包含无法完全迁移到服务端、同时具有高业务价值且适合稳定虚拟化的决策。状态一次性消费、回调结果与本地会话的绑定、账户切换前的状态机和高价值功能入口映射,通常比 Uri 解析、Intent 分发或网络库封装更接近目标。最终名单仍需依据项目代码、调用图和兼容测试决定。
VMP 结论要与协议结论分开。协议门禁回答 host、path、state、PKCE、重定向和服务器授权是否正确;保护门禁回答选定函数在保护前后行为是否等价、异常分支是否保留以及性能是否可接受。任何一边未通过都应停线,不能用保护强度报告抵消 OAuth 缺陷,也不能用协议正确证明客户端决策不值得保护。
| 层 | 责任主体 | 主要证据 | VMP 作用 |
|---|---|---|---|
| 域名关联 | Manifest、网站与平台 | host、assetlinks 与证书 | 不适用 |
| 系统路由 | Android 链接策略 | 验证状态与设备设置 | 不适用 |
| URL 解析 | 应用协议适配层 | 严格 allowlist 与错误处理 | 通常保持透明 |
| OAuth 校验 | 客户端与授权服务器 | state、PKCE、重定向 | 只保护不替代 |
| 业务会话 | 应用与业务服务端 | 会话绑定和服务端回执 | 可选高价值函数 |
| 最终授权 | 业务服务端 | scope、权益与撤销 | 客户端 VMP 无权替代 |
App Links 平台信任链必须在 VMP 之外先通过
Android App Links overview 说明 App Links 将 Manifest 中的 URL 声明、网站 Digital Asset Links 文件和应用签名证书关联起来。Manifest 负责声明组件与 intent filter,assetlinks.json 由网站发布并列出允许关联的应用身份,系统据此决定 HTTPS 链接能否直接进入应用。任何主机、路径、证书指纹或发布位置错误都不是虚拟化回调代码能够修复的问题。
Verify Android App Links 给出重置、重新验证和读取域名状态的系统操作路径。发布回执应绑定应用候选、签名证书、设备、系统版本、域名和验证状态,并区分 release 与本地调试签名。看到应用能够被手工选择打开,不代表域名已经验证;看到一个域名通过,也不能外推同一 intent filter 中的所有主机与路径。
Test Android App Links 强调逐一核对 Manifest 主机、assetlinks.json、签名指纹和设备链接策略。测试应覆盖冷启动、应用已在前台、返回栈、用户关闭关联、错误路径、额外查询参数和多个匹配应用。平台验证通过只证明路由关联的一部分,不证明回调参数、登录状态或业务授权正确,二者要用不同状态字段记录。
| 对象 | 必须核对 | 失败表现 | 正确处置 |
|---|---|---|---|
| Manifest | https、host、path 与组件 | 链接不匹配或误匹配 | 修正声明并重建 |
| assetlinks.json | 包名、关系与证书指纹 | 域名无法验证 | 修正网站文件 |
| 签名 | 当前候选证书摘要 | debug 通过但 release 失败 | 分别登记身份 |
| 系统状态 | 每个域名验证结果 | 进入浏览器或选择器 | 重置并重新验证 |
| 用户策略 | 设备上的链接偏好 | 已验证仍不直达 | 记录设备设置 |
| 路径矩阵 | 全部允许和拒绝 URL | 局部误路由 | 逐模式测试 |
URL 解析层应当小、严格并且容易审计
回调入口接收到 Intent 后,应先把外部输入当成不可信数据。解析层只负责确认 action、scheme、规范化 host、精确 path、参数集合和重复参数策略,并把未知字段保留为拒绝或忽略的明确决定。不要在 Uri 解析过程中直接写登录状态、切换账户或开启高价值功能,否则协议错误与业务副作用会纠缠,负向测试也难以覆盖。
这层通常不适合大范围 VMP。Android 生命周期、导航框架、反射、序列化和第三方 OAuth SDK 可能依赖稳定方法签名、注解或生成代码;过度保护会扩大兼容回归面。更重要的是,解析规则需要持续审计。让 allowlist、重复参数拒绝和错误分类保持短小透明,安全人员才能直接确认外部输入在进入业务层前经过哪些条件。
透明不等于没有保护。解析函数可以保留完整性校验、日志脱敏和调用约束,但不能把不可见性当成输入验证。建议输出一个最小结果对象,只包含回调类型、一次性 transactionId、授权码引用、state 校验状态和错误枚举,不把完整 URL、令牌或用户数据传入后续日志。VMP 候选只接收这个已规范化对象。
| 检查项 | 允许策略 | 拒绝条件 | 输出字段 |
|---|---|---|---|
| scheme | 仅 https 或批准回调 | 其他 scheme | callbackType |
| host | 规范化精确 allowlist | 子串或后缀猜测 | hostId |
| path | 精确或显式模板 | 路径穿透和未知路由 | routeId |
| query | 已声明参数集合 | 重复关键参数 | 参数投影 |
| 错误 | 协议定义错误字段 | 错误和 code 同时冲突 | errorCode |
| 日志 | 摘要和追踪 ID | 完整 URL、code 或令牌 | traceId |
公共客户端不能靠 VMP 保存 OAuth 客户端秘密
RFC 8252 OAuth 2.0 for Native Apps 将原生应用视为公共客户端,建议使用外部 user-agent 与 PKCE,而不是依赖客户端秘密。APK 可被用户取得和分析,因此嵌入其中的静态 secret 无法获得服务器秘密相同的信任级别。把 secret 字符串或解密函数放进 VMP 只会改变提取成本,不能把它升级为可长期信任的凭据。
PKCE 需要为每次授权生成高熵 code_verifier,并把相应 challenge 发送到授权端点,令牌端点再核对 verifier。state 用于把回调与发起交易绑定,两者职责不同,不能互相替代。处理器应保存最小的短期交易状态,回调时一次性消费;应用被杀、用户取消、多个并行登录和超时都要有明确状态转移。
授权码在哪里交换要服从具体架构。原生公共客户端可以按协议使用 PKCE 与授权服务器交互;若业务服务端还要建立自己的会话、限制 scope、绑定设备风险或执行权益判断,这些最终决定必须在服务端完成。客户端不能因为做了 VMP 就持有服务端 client secret,也不能自行扩大授权范围或延长会话。
| 对象 | 正确责任 | VMP 是否替代 | 失败处置 |
|---|---|---|---|
| client secret | 仅可信机密客户端保存 | 不能 | 从 APK 移除 |
| code_verifier | 单次授权临时生成 | 不能替代熵和生命周期 | 过期即销毁 |
| state | 绑定发起交易并一次性消费 | 可保护状态逻辑 | 错配或重放拒绝 |
| redirect URI | 严格注册和匹配 | 不能 | 不匹配拒绝 |
| authorization code | 短期且绑定交易 | 不应记录完整值 | 失败后作废 |
| 业务 scope | 服务端最终决定 | 不能 | 客户端只展示结果 |
状态消费和业务会话绑定是更清晰的 VMP 候选
RFC 9700 OAuth Security BCP 关注重定向、令牌重放、授权码注入和不安全 grant 等部署风险。对应到客户端,回调处理器需要验证当前是否存在匹配交易、state 是否一致、交易是否过期或已经消费、授权服务器上下文是否符合预期,并把失败变成不可继续的状态。VMP 可以增加静态修改这些决策的成本,但无法替代授权服务器对 code、PKCE 和重定向的验证。
更适合保护的第一类函数是一次性状态机:它接收已规范化 transactionId 与当前持久状态,只允许 pending 进入 consuming,再根据服务端回执进入 completed 或 failed,并拒绝第二次消费。第二类是业务会话绑定:确认服务端返回的账户、租户、设备上下文与发起交易一致,再原子替换本地会话。第三类是高价值权益映射,但最终权益值必须来自服务端签发结果。
候选函数应具有窄输入、明确输出、少反射、少框架回调和可独立测试的特征。保护边界内不要直接持有 Activity、Intent、WebView、数据库游标或网络响应对象,而应使用稳定的数据投影。这样既减小虚拟化兼容风险,也避免把大量公共协议和生命周期代码拖入保护范围。无法隔离副作用的函数应先重构和测试,再决定是否保护。
| 函数类型 | 价值 | 适配性 | 前置条件 |
|---|---|---|---|
| state 一次性消费 | 防止本地状态机被简单改写 | 高 | 服务端仍验证协议 |
| 业务会话绑定 | 阻止错误账户映射 | 高 | 输入来自可信回执 |
| 权益映射 | 保护高价值业务决策 | 中到高 | 权益由服务端签发 |
| URL allowlist | 协议输入过滤 | 低到中 | 保持可审计和可测试 |
| Activity 回调 | 生命周期胶水 | 低 | 通常排除 |
| OAuth SDK/网络库 | 第三方公共实现 | 低 | 不纳入自有 VMP |
明确排除项,避免把协议缺陷埋进虚拟化层
Manifest 与 assetlinks.json 是声明和网站配置,不是 VMP 对象;系统验证与用户链接偏好也不在应用控制范围。Uri、Intent、浏览器和 OAuth SDK 适配代码需要频繁跟随平台升级,通常应保持小而透明。把这些层整体保护会增加堆栈诊断和兼容测试成本,却不会修复错误 host、错误证书或不严格的重定向策略。
令牌存储、传输和服务端校验也不能被一段受保护函数概括。令牌应使用平台和协议允许的安全机制管理,日志不得写入 code、verifier、access token 或完整回调 URL;传输安全、撤销、过期和 scope 由相应层负责。VMP 不提供网络信道保密,也不能阻止已获授权的客户端进程在运行时使用自己的会话。
不要保护包含大量反射、动态代理、序列化生成代码、协程状态机或框架注入入口的宽函数,除非当前工具链有项目级证据证明兼容。保护名单应精确到方法并记录调用方、异常语义、线程、性能预算和回滚开关。没有保护前后测试回执时,只能写成候选边界,不能声称已经提高逆向成本或阻断攻击。
| 对象 | 排除原因 | 替代措施 | 重新评估条件 |
|---|---|---|---|
| Manifest/assetlinks | 不属于可执行业务函数 | 配置与签名门禁 | 不进入 VMP |
| Activity 生命周期 | 框架耦合和回调复杂 | 保持薄入口 | 隔离后再评估 |
| Uri 解析器 | 需要透明审计 | 严格 allowlist 和测试 | 仅保护窄策略 |
| OAuth 第三方库 | 升级与兼容责任外部 | 锁定版本和回归 | 自有封装窄函数 |
| 网络传输 | VMP 不提供信道安全 | TLS 与服务端校验 | 不作为保护目标 |
| 生成/反射代码 | 易受变换影响 | keep 规则和边界隔离 | 有真实候选证据 |
发布测试要把 App Link、OAuth 和 VMP 等价性分成三套回执
App Link 回执先验证 Manifest、assetlinks、release 证书和系统域名状态,再覆盖全部允许与拒绝 URL。测试至少包含冷启动、前台、后台恢复、用户取消、浏览器返回、错误 path、重复参数和未知 host。每条结果绑定 APK 摘要、设备、系统、域名与链接策略,不能只保存一张成功打开页面的截图。
OAuth 回执覆盖正常交易和负向用例:state 缺失、state 错配、重复消费、过期交易、错误重定向、授权错误、并行登录和进程被杀。RFC 8252 与 RFC 9700 提供协议方向,但业务 scope、会话时长、账户切换和服务端风险策略仍需项目定义。没有授权服务器日志或服务端回执时,客户端页面跳转成功不能登记为登录链路通过。
VMP 回执只针对最终名单,比较保护前后输入输出、异常类型、线程、冷启动和回调时延,并在目标 API、ABI 与厂商设备上执行。任何差异先判断是保护变换还是原流程缺陷,不能通过放宽 state、host 或错误处理让测试变绿。发布记录同时引用协议回执和保护回执,缺一项就保持待确认。
- App Link 域名、证书与系统验证逐项有回执
- 允许和拒绝 URL 模式均覆盖冷启动与返回栈
- state、PKCE、重放和并行登录负向用例齐备
- 服务端会话、scope 和账户绑定拥有独立回执
- VMP 名单精确到方法且排除框架与第三方库
- 保护前后等价性绑定当前 APK 和设备矩阵
用策略清单阻止错误主机、公共秘密和越界 VMP 名单
下面的 Python 示例读取一份 App Link 登录边界 JSON。它检查 Manifest 主机与 assetlinks 主机一一对应、release 证书摘要格式、回调必须使用 HTTPS、严格 host 与 path、拒绝重复参数、要求 state 与 PKCE、禁止嵌入 client secret,并验证 VMP 候选只来自 state-consumption、business-session-binding 或 entitlement-mapping 三类窄函数。输入缺失或边界冲突会返回非零状态。
脚本只验证清单内部约束,不会读取设备的真实 App Link 状态,也不能证明 assetlinks.json 已在线生效、授权服务器正确实现 PKCE 或候选函数适合当前 VMP 工具链。生产流程应从最终 Manifest、网站文件、签名证书、代码所有权和构建配置生成各字段,再把静态通过结果与系统验证、OAuth 负向测试及保护前后真机回执绑定。
申请 App Link 登录链路的商业加固评估时,可准备最终 APK、Manifest 与 assetlinks 证据、release 证书摘要、URL 矩阵、OAuth 流程、状态机、服务端授权边界、候选方法和设备回归计划,再从御盾中央平台提交申请。真实候选和测试回执到位前,不宣称 client secret 被保护、重放被阻断或登录链路兼容通过。
- Manifest host 与在线 assetlinks 主机逐项对应
- 证书摘要来自当前 release 候选而非调试包
- OAuth 回调使用 HTTPS、严格匹配、state 与 PKCE
- APK 中不存在被当作服务器秘密的 client secret
- VMP 名单只包含窄业务决策函数和数据投影
- 静态清单通过后继续执行系统、协议和真机回执
from pathlib import Path
import json
import re
import sys
CERT = re.compile(r"^[0-9a-f]{64}$")
ALLOWED_VMP_ROLES = {
"state-consumption",
"business-session-binding",
"entitlement-mapping",
}
FORBIDDEN_VMP_ROLES = {
"activity-lifecycle",
"uri-parser",
"oauth-library",
"network-transport",
}
def load_policy(path_text):
path = Path(path_text)
if not path.is_file():
raise SystemExit(2)
try:
value = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
raise SystemExit(2)
if not isinstance(value, dict):
raise SystemExit(2)
return value
def require_strings(value):
return isinstance(value, list) and value and all(isinstance(item, str) and item for item in value)
def normalized_hosts(value):
if not require_strings(value):
raise SystemExit(2)
hosts = [item.lower().rstrip(".") for item in value]
if len(hosts) != len(set(hosts)) or any("/" in item for item in hosts):
raise SystemExit(2)
return set(hosts)
if len(sys.argv) != 2:
raise SystemExit(2)
policy = load_policy(sys.argv[1])
manifest = policy.get("manifest")
assetlinks = policy.get("assetlinks")
oauth = policy.get("oauth")
functions = policy.get("functions")
if not all(isinstance(value, (dict, list)) for value in (manifest, assetlinks, oauth, functions)):
raise SystemExit(2)
manifest_hosts = normalized_hosts(manifest.get("httpsHosts"))
linked_hosts = set()
for link in assetlinks:
if not isinstance(link, dict) or not isinstance(link.get("host"), str):
raise SystemExit(3)
certificate = link.get("releaseCertificateSha256", "").lower()
if CERT.fullmatch(certificate) is None:
raise SystemExit(3)
linked_hosts.add(link["host"].lower().rstrip("."))
if manifest_hosts != linked_hosts:
raise SystemExit(3)
required_oauth = {
"redirectScheme": "https",
"strictHostMatch": True,
"strictPathMatch": True,
"rejectDuplicateParameters": True,
"requireState": True,
"consumeStateOnce": True,
"requirePkce": True,
"embeddedClientSecret": False,
}
for field, expected in required_oauth.items():
if oauth.get(field) != expected:
raise SystemExit(4)
if not require_strings(oauth.get("redirectPaths")):
raise SystemExit(4)
seen = set()
protected = []
for function in functions:
if not isinstance(function, dict) or not isinstance(function.get("name"), str):
raise SystemExit(5)
if function["name"] in seen:
raise SystemExit(5)
seen.add(function["name"])
role = function.get("role")
use_vmp = function.get("useVmp")
if role in FORBIDDEN_VMP_ROLES and use_vmp is True:
raise SystemExit(5)
if use_vmp is True:
if role not in ALLOWED_VMP_ROLES or function.get("inputProjection") is not True:
raise SystemExit(5)
protected.append(function["name"])
if not protected:
raise SystemExit(5)
print(json.dumps({"status": "eligible-for-project-review", "hosts": sorted(manifest_hosts), "vmpCandidates": protected}, ensure_ascii=False))事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| App Links 将 Manifest URL、Digital Asset Links 和应用签名证书关联起来。 | Android App Links overview 描述 HTTPS 链接、网站声明和应用身份之间的关系。 | 域名关联成功不代表应用内参数校验、OAuth 交易、登录和业务授权正确。 |
| 系统可以重置、重新验证并读取每个 App Link 域名的验证状态。 | Verify Android App Links 给出域名验证状态的检查和重新验证方法。 | 系统状态不能替代冷启动、返回栈、用户策略和异常参数的端到端回归。 |
| App Link 测试需要逐一核对 Manifest 主机、assetlinks、签名指纹和设备链接策略。 | Test Android App Links 描述关联文件、证书与设备链接测试对象。 | 单一设备、域名或路径通过不能代表全部 URL 模式与设备状态。 |
| Manifest 定义组件、intent filter、权限、SDK 约束和应用元数据。 | Android app manifest 描述应用清单的静态声明范围。 | 静态清单不能证明运行时访问控制、URL 参数处理或服务端授权没有被放宽。 |
| 原生应用属于公共客户端,应使用外部 user-agent 与 PKCE,而不是依赖客户端秘密。 | RFC 8252 OAuth 2.0 for Native Apps 给出原生应用授权的安全实践。 | RFC 8252 不定义具体业务 API 的最终 scope、会话时长和商业权限模型。 |
| OAuth 部署需要限制不安全重定向、令牌重放、授权码注入和不安全 grant。 | RFC 9700 OAuth Security BCP 汇总 OAuth 部署的当前安全建议与攻击类别。 | 安全 BCP 不证明当前客户端或授权服务器已正确实施,也不决定业务权益。 |
| 状态一次性消费、业务会话绑定和高价值权益映射比生命周期入口更适合作为 VMP 候选。 | 工程判断:这些函数承载窄业务决策,可使用最小数据投影并独立做保护前后等价测试。 | 适合性仍需当前工具链、调用图、异常语义、性能预算和真实设备回执确认。 |
| VMP 只能增加客户端决策的静态分析与修改成本,不能替代协议和服务端授权。 | 工程判断:assetlinks、系统验证、授权服务器、scope 和撤销均位于客户端虚拟化边界之外。 | 没有当前候选攻防与兼容证据时,不量化逆向成本,不宣称重放或攻击已被阻断。 |
工程常见问题
把整个 App Link 回调 Activity 做 VMP 是否更安全?
通常不是。Activity 生命周期、导航、Uri 解析和第三方 OAuth 库耦合面大,且 VMP 不能修复域名关联、重定向或服务端授权错误。应优先保护窄业务决策函数。
assetlinks.json 验证通过,为什么仍要校验回调参数?
域名关联只解决 HTTPS 链接与应用身份的路由关系,不验证 state、授权码、错误分支、账户交易或业务 scope。应用和授权服务器仍需完成协议校验。
能否把 OAuth client secret 放进 VMP 后随 APK 发布?
不应把原生公共客户端中的静态 secret 当作服务器秘密。VMP 只能增加提取成本,不能改变用户可取得 APK 和运行客户端代码的事实,应采用 PKCE 与合适的服务端设计。
state 与 PKCE 是否可以只保留一个?
不能简单互换。state 用于绑定发起交易与回调,PKCE 把授权码兑换绑定到本次客户端秘密 verifier。两者应按协议和具体威胁模型分别实现。
哪些登录代码最值得先做 VMP 评估?
优先评估一次性状态消费、服务端回执与本地业务会话绑定、账户切换状态机和高价值权益映射。函数应窄、少框架依赖、输入投影稳定并可独立回归。
提交 App Link 登录加固评估需要哪些资料?
准备最终 APK、Manifest 与 assetlinks、release 证书摘要、URL 矩阵、OAuth 流程、状态机、服务端授权边界、候选方法和设备回归计划,再通过御盾中央平台提交申请。