先看结论与判断条件
- 平台证明回答应用实例与运行环境的风险问题,模型权益回答谁可在什么条件下使用哪个模型,两者必须由服务端显式组合。
- 挑战值、业务请求摘要、用户会话、模型版本和权益令牌需要共享可核对的请求标识,任一字段不一致都应拒绝。
- App Attest 与 Play Integrity 的响应都应由后端验证,客户端展示成功状态不能成为模型密钥或下载地址的放行依据。
- DPoP 可以把访问令牌绑定到客户端持有的密钥,但不能证明设备绝对可信,也不能替代受众、期限和业务权限校验。
- 模型文件的摘要、版本和更新元数据应进入权益决策,避免合法用户拿旧授权加载未批准、回滚或混搭的模型产物。
- 验收记录必须区分平台事实、工程设计和项目实测;没有真实证明回执与候选包证据时,只能确认方案完整,不能宣称攻击已被阻断。
先拆开应用实例证明与模型权益
应用实例证明与模型授权常被画成一个“可信设备即可下载”的判断框,但它们回答的是不同问题。平台证明尝试说明请求是否来自预期应用实例,并返回与平台环境相关的风险信号;模型权益则要判断当前用户、账户、套餐、地区、模型版本、使用场景和有效期是否满足业务规则。前者通过不代表后者自然成立,后者有效也不应跳过前者要求的请求绑定。
把两者混成一个布尔值会产生难以审计的漏洞。例如,客户端可能拿一次成功证明反复请求多个模型版本,也可能把甲会话取得的证明结果与乙账户的权益令牌拼接。服务端若只检查“证明通过”和“令牌未过期”,却没有核对它们是否属于同一业务动作,就无法区分合法重试与跨请求挪用。设计文档应把每个输入、签发方、受众、时效和绑定字段逐项列出。
合理的信任边界是:移动端负责生成平台要求的证明材料、持有本机密钥并提交业务请求;证明服务负责给出可验证的完整性信号;授权服务核对用户权益、请求上下文与模型身份;模型分发或推理服务只接受授权服务签发且受众正确的短期令牌。任何客户端本地开关、缓存状态或界面提示都只能改善体验,不能替代服务端决策。
| 对象 | 主要回答 | 不能单独证明 | 服务端最低检查 |
|---|---|---|---|
| 应用实例证明 | 请求是否来自预期应用实例及平台环境 | 用户已购买模型或允许访问该版本 | 证明链、挑战、请求摘要、时效与重放状态 |
| 用户会话 | 当前请求对应哪个已认证主体 | 客户端实例没有被替换或复制 | 会话状态、主体、认证强度与撤销记录 |
| 模型权益 | 主体可使用哪个模型及操作 | 请求来自可信应用实例 | 模型标识、版本、动作、限制与到期时间 |
| 持有证明令牌 | 调用方持有与令牌绑定的密钥 | 设备环境或业务身份绝对可信 | 公钥拇指印、方法、目标 URI、时间和唯一标识 |
| 模型产物元数据 | 下载或加载对象的版本和摘要 | 用户仍具备当前商业权限 | 签名、版本、过期、一致性与目标摘要 |
| 服务端审计事件 | 系统为何允许或拒绝一次动作 | 所有下游执行都已成功 | 决策码、绑定标识、策略版本与证据引用 |
用规范化业务请求生成唯一绑定材料
绑定的起点不是平台 API,而是一份能够稳定序列化的业务请求。它至少包含请求标识、主体会话引用、操作类型、模型标识、模型版本、客户端生成的公钥引用、服务端挑战和过期时间。字段顺序、编码、空值处理和字符串规范必须固定,否则客户端与服务端计算的摘要会因为表示差异而不一致。摘要输入还应带协议版本,防止未来增删字段时旧客户端产生含义不同但格式相同的请求。
请求标识应由服务端生成或由服务端认可,并只在短窗口内使用一次。挑战记录需要保存签发时间、用途、已消费状态和预期会话,验证成功后以原子方式标记消费。仅检查挑战字符串是否存在不够,因为并发提交可能在消费状态落库前同时通过。数据库更新应带“当前仍未消费且未过期”的条件,只有成功更新一行的请求才进入后续授权。
Play Integrity 标准请求提供 requestHash,用于把完整性响应绑定到业务请求。工程上应对规范化请求字节计算摘要,把同样字节或可重建字段保存在服务端,并在收到响应后重算。Apple App Attest 的服务端验证同样需要让断言覆盖与这次操作相关的客户端数据。两种平台的字段与流程不同,但共同原则是证明必须靠近受保护动作,不能使用一份脱离业务上下文的长期“设备可信”缓存。
| 字段 | 来源 | 参与的校验 | 失败处理 |
|---|---|---|---|
| request_id | 授权服务 | 挑战、证明、会话和权益记录必须一致 | 不一致直接拒绝并记录决策码 |
| challenge | 授权服务 | 证明材料必须覆盖当前未消费挑战 | 过期或已消费时重新发起流程 |
| subject_ref | 认证服务 | 会话主体与权益主体必须一致 | 禁止客户端自行替换主体 |
| model_id 与 version | 业务请求 | 权益范围与产物元数据必须精确匹配 | 版本缺失或越界时不下发令牌 |
| operation | 业务请求 | 下载、解密、推理等动作分别授权 | 令牌不得跨动作复用 |
| audience | 授权服务 | 令牌只能交给指定资源服务 | 资源服务拒绝其他受众 |
| expires_at | 授权服务 | 挑战、证明与权益均在各自窗口内 | 过期后重新证明而非本地延长 |
分别处理 Apple 与 Android 的平台证明
Apple 的 App Attest 流程要求服务器验证证明对象与后续断言,并将断言关联到具体服务器请求。实现时应保存应用实例公钥、平台返回材料、计数或重放相关状态以及验证结果,但不要把客户端返回的“验证成功”当成事实。服务端还要核对挑战是否属于当前会话和模型动作。应用升级、密钥重新生成、设备迁移或平台服务暂时不可用时,系统需要明确是重新注册、风险降级还是拒绝,而不是静默放宽。
Play Integrity 的标准请求强调在受保护动作附近准备完整性令牌,并可通过 requestHash 绑定业务请求。后端解码并验证响应后,还需将返回的应用、账户或设备相关信号映射到自己的风险策略。Google 的概览把完整性 API 定位为整体反滥用策略的一部分,这意味着单一 verdict 不应被提升为绝对可信根;异常信号、调用频率、账户状态与模型权益仍要共同参与最终决定。
跨平台层不应强行把两套响应压成相同字段后丢弃原始语义。可建立统一的决策输入,例如 provider、verification_status、request_binding_status、freshness_status、replay_status 和 policy_tier,同时保存平台特定证据引用。这样上层权益服务可以复用状态机,底层验证器仍按官方规则处理。遇到新平台信号或字段含义变化时,只调整适配器和策略版本,不篡改历史审计事件。
- 证明材料只由后端验证,客户端结果不直接放行
- 每份证明都能反查到当前挑战和业务请求摘要
- 挑战消费使用原子条件更新避免并发重放
- 平台特定验证证据保留引用而非只存布尔值
- 平台服务异常时采用明确的拒绝或有限降级策略
- 应用实例密钥轮换、丢失和重新注册都有状态迁移
- 策略日志区分验证失败、权益失败和资源服务失败
让服务端状态机完成最终授权
授权服务应把一次模型请求拆成可观察的状态:挑战已签发、证明已验证、挑战已消费、会话已确认、权益已匹配、产物身份已确认、短期令牌已签发以及资源服务已接受。状态转换必须由服务端事件触发,并携带策略版本与失败原因。客户端可以重试网络请求,但不能直接把状态从“证明待验证”推进到“权益有效”,也不能依靠修改本地时间延长服务器记录的有效期。
放行规则应采用全部必要条件同时成立,而不是给信号打一个含糊总分。高价值模型下载可以要求请求绑定通过、证明新鲜、会话有效、权益覆盖精确版本、模型元数据未过期且受众匹配。低风险的公开模型目录查询可以不要求平台证明。按动作分级能减少无意义证明调用,也能避免为追求可用性而在核心下载接口上设置不透明的“失败也放行”。
拒绝结果需要可恢复但不能泄露策略细节。客户端可接收稳定的公开错误类别,如会话需刷新、证明需重试、版本不可用或权限不足;内部审计则保留更具体的验证步骤和策略命中项。不要向客户端返回哪个完整性字段失败、风险阈值或可用于试探策略的细粒度分数。支持人员凭请求标识查询内部事件即可,不应要求用户上传完整证明对象或令牌。
| 当前状态 | 必要输入 | 允许转移 | 拒绝条件 |
|---|---|---|---|
| 挑战已签发 | request_id、会话、模型动作、到期时间 | 进入证明待验证 | 主体或模型动作缺失 |
| 证明待验证 | 平台材料、挑战、请求摘要 | 进入证明通过 | 签名、绑定、时效或重放检查失败 |
| 证明通过 | 有效会话与主体 | 进入权益待匹配 | 会话撤销、主体不一致或认证不足 |
| 权益待匹配 | 模型、版本、操作与商业策略 | 进入产物待确认 | 套餐、地区、版本或动作不覆盖 |
| 产物待确认 | 签名元数据、摘要、版本与期限 | 签发短期权益令牌 | 回滚、过期、混搭或摘要不匹配 |
| 令牌已签发 | 受众、持有密钥与有效期 | 资源服务接受请求 | 受众错误、持有证明无效或令牌过期 |
| 资源已交付 | 服务端使用回执 | 登记完成或受限重试 | 重复下载超限或请求上下文变化 |
用 DPoP 限制权益令牌被搬到别处使用
短期令牌如果只是 bearer token,拿到它的一方通常可以在有效期内直接使用。RFC 9449 定义的 DPoP 让客户端针对 HTTP 请求生成持有证明,并把访问令牌绑定到相应公钥。资源服务检查证明的方法、目标 URI、时间、唯一标识与令牌确认字段,可以降低令牌被复制到另一客户端后重放的风险。模型下载和远程推理接口可把这类发送者约束作为纵深防护。
DPoP 不等于设备完整性证明。密钥可能由被控制的应用进程合法调用,平台证明也可能只覆盖某个时间点。两者应在授权时通过请求标识与公钥引用关联:平台证明验证的是当前应用实例和业务请求,权益令牌记录允许的模型与动作,DPoP 证明调用方仍持有签发时绑定的密钥。任何一层缺失都不能用另一层的成功来补齐。
RFC 9700 汇总了 OAuth 部署中的重放、授权码注入、重定向和不安全授权模式等风险。对于模型权益接口,应缩小令牌受众和 scope,不把下载、解密、远程推理、管理配置合并为一个宽权限令牌;刷新流程也要重新检查撤销状态和高风险上下文。工程判断是,高风险模型版本发生变化时应重新授权,不让旧令牌通过模糊版本范围自动覆盖新资产。
| 声明或证明 | 绑定对象 | 资源服务检查 | 不能替代 |
|---|---|---|---|
| audience | 指定下载或推理服务 | 当前服务标识精确匹配 | 用户权益与模型版本检查 |
| scope 或 operation | 下载、解密、推理等动作 | 请求动作位于允许集合 | 平台证明与会话有效性 |
| model_id 与 version | 批准的具体模型产物 | 路径、元数据和摘要对应同一版本 | 更新元数据签名验证 |
| subject | 用户或业务主体 | 与当前会话和资源所有者一致 | 应用实例完整性判断 |
| confirmation key | 客户端持有的公钥 | DPoP 证明与确认字段匹配 | 绝对设备可信或防止进程内滥用 |
| expiry 与 issued-at | 有限使用窗口 | 服务器时间与撤销状态有效 | 请求唯一性和挑战消费 |
| request_id | 整条授权事务 | 审计事件与模型请求一致 | 令牌签名和受众校验 |
把模型版本和更新元数据纳入权益判断
模型权益不能只写一个产品名称。量化方式、分词器、适配器、系统提示模板和运行时依赖变化,都可能让同名模型成为不同交付对象。授权服务应使用不可歧义的 model_id、version 与 artifact_digest,资源服务根据这组三元组返回产物。客户端收到文件后仍需验证签名元数据与摘要,不能因为下载连接来自合法域名就跳过本地身份检查。
The Update Framework 的规范通过角色化签名元数据、版本、过期时间和一致性关系来处理更新中的回滚、冻结与混搭风险。移动模型系统可以借用这些设计原则,把目标元数据、快照引用和到期策略纳入下载验证。不过 TUF 解决的是更新信任链与元数据一致性,不会判断某个账户是否购买模型,也不会替权益服务决定允许的推理次数。
版本绑定还要覆盖缓存和并发。若客户端已经缓存旧模型,服务端不能只返回“用户有权限”而忽略旧版本是否仍获准;若新版本正在分批发布,权益令牌必须指明允许的版本集合或唯一版本,并与下载元数据一致。回滚只应由签名且未过期的策略显式批准。任何本地修改版本号、复制元数据或复用旧下载地址的行为都不应改变服务端授权结果。
#!/usr/bin/env python3
import json
import re
import sys
from pathlib import Path
REQUEST_PATTERN = re.compile(r"^[a-f0-9]{32}$")
MODEL_PATTERN = re.compile(r"^[a-z0-9][a-z0-9._-]{2,63}$")
ALLOWED_OPERATIONS = {"download", "remote_inference"}
def require_text(record, field):
value = record.get(field)
if not isinstance(value, str) or not value.strip():
print("missing field: " + field, file=sys.stderr)
raise SystemExit(3)
return value.strip()
def validate(record):
request_id = require_text(record, "request_id")
if not REQUEST_PATTERN.fullmatch(request_id):
print("invalid request_id", file=sys.stderr)
raise SystemExit(4)
bound_ids = {
require_text(record, "attestation_request_id"),
require_text(record, "session_request_id"),
require_text(record, "entitlement_request_id"),
}
if bound_ids != {request_id}:
print("request binding mismatch", file=sys.stderr)
raise SystemExit(5)
model_version = require_text(record, "model_version")
entitlement_version = require_text(record, "entitlement_model_version")
if model_version != entitlement_version:
print("model version mismatch", file=sys.stderr)
raise SystemExit(6)
if not MODEL_PATTERN.fullmatch(model_version):
print("unsafe model version", file=sys.stderr)
raise SystemExit(7)
operation = require_text(record, "operation")
if operation not in ALLOWED_OPERATIONS:
print("operation not allowed", file=sys.stderr)
raise SystemExit(8)
audience = require_text(record, "audience")
expected_audience = require_text(record, "expected_audience")
if audience != expected_audience:
print("audience mismatch", file=sys.stderr)
raise SystemExit(9)
expires_at = record.get("expires_at")
evaluated_at = record.get("evaluated_at")
if not isinstance(expires_at, int) or not isinstance(evaluated_at, int):
print("timestamps must be integers", file=sys.stderr)
raise SystemExit(10)
if expires_at <= evaluated_at:
print("entitlement expired", file=sys.stderr)
raise SystemExit(11)
def main():
if len(sys.argv) != 2:
print("Usage: entitlement_check.py INPUT_JSON", file=sys.stderr)
raise SystemExit(2)
input_path = Path(sys.argv[1])
if not input_path.is_file():
print("input file not found", file=sys.stderr)
raise SystemExit(2)
try:
record = json.loads(input_path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
print("cannot read input: " + str(exc), file=sys.stderr)
raise SystemExit(2)
if not isinstance(record, dict):
print("input must be an object", file=sys.stderr)
raise SystemExit(2)
validate(record)
print("binding accepted")
if __name__ == "__main__":
main()设计失败、重试、离线和撤销路径
证明调用会遇到网络中断、平台限流、系统时间偏差、应用实例密钥丢失和服务端验证器升级。失败策略必须按动作风险设计。公开目录可以返回缓存,已下载且仍在有效窗口内的模型可以维持受限功能,高价值新模型下载则应在证明或权益状态不确定时拒绝。所谓有限降级必须写清允许的模型版本、截止时间、功能范围和服务器撤销优先级,不能用一个长期本地开关表示。
重试需要保持幂等。客户端重发同一 request_id 时,服务端可以返回同一最终结果或明确告知挑战已消费,但不能重复签发语义不同的权益令牌。若业务上下文、会话、模型版本或持有密钥发生变化,应创建新请求并重新证明。网络超时后先查询事务状态,再决定是否提交新请求,可以避免客户端把未收到响应误判为服务端未处理。
撤销至少覆盖账户权益撤销、应用实例密钥撤销、模型版本撤回、令牌撤销和策略紧急收紧。资源服务不能只依赖令牌自然过期;对高风险事件可检查短期撤销缓存或授权服务状态。离线模型无法获得实时撤销,因此离线窗口本质上是业务接受的剩余风险。设计评审应记录最大窗口和重新联网条件,而不是声称本地加密可以消除离线授权风险。
- 每种失败码都有公开恢复动作和内部审计原因
- 同一请求重试不会签发语义不同的权益令牌
- 请求上下文变化时强制生成新挑战与请求标识
- 平台服务不可用时按动作风险决定拒绝或有限降级
- 离线窗口绑定明确模型版本、截止时间和功能范围
- 账户、实例、模型、令牌和策略均有独立撤销入口
- 客户端时间只作体验提示,最终期限由服务端时间判断
用可复核证据验收整条授权链
验收不应只截取一次“授权成功”响应。测试包需要固定应用版本与签名身份,建立请求信封样本,记录挑战签发、平台验证、会话核对、权益匹配、模型元数据确认、令牌签发和资源服务接受的连续事件。每个事件只保存公开安全的请求引用、状态、策略版本和时间,不保存完整令牌或平台证明对象。这样既能复盘决策,也不会让日志成为新的凭据仓库。
负向用例应围绕绑定关系,而不是尝试复现攻击链。可以交换请求标识、替换模型版本、改变令牌受众、重复使用已消费挑战、使用过期事务或撤销后的主体,确认每一种不一致都在预期层被拒绝。测试结论要写清观察点:授权服务拒绝不等于模型文件已从设备删除,资源服务拒绝也不证明客户端内存中的历史明文不存在。不同责任边界需要不同证据。
当前没有具体应用候选包、真实平台证明回执、权益数据库快照和资源服务日志,因此本文只能给出可执行架构与检查清单,不能断言某个产品已经阻断重放或模型盗用。项目落地时应把同一候选身份、测试账户、策略版本和事件链装入受控证据包,门禁通过后再扩大流量;任何重新构建、重签名或模型元数据变化都需要重新验收。
| 证据项 | 验证问题 | 合格形态 | 结论边界 |
|---|---|---|---|
| 候选身份 | 证明是否对应实际交付应用 | 版本、签名身份与受控摘要一致 | 不代表业务授权策略正确 |
| 请求信封 | 各层是否使用同一 request_id | 规范化字段和摘要可重算 | 不包含真实令牌与完整证明对象 |
| 挑战消费记录 | 并发与重试是否可能重放 | 只有一次原子消费成功 | 只证明服务端状态机行为 |
| 平台验证事件 | 证明是否绑定当前业务请求 | 验证状态、时效与证据引用完整 | 平台信号不是绝对设备可信 |
| 权益决策事件 | 主体、动作和模型版本是否覆盖 | 策略版本与允许或拒绝原因清楚 | 不证明资源服务已经执行 |
| 资源服务记录 | 受众、持有证明和产物身份是否一致 | 请求引用与模型摘要可追溯 | 不证明客户端历史缓存已清除 |
| 负向用例 | 字段替换、过期和撤销是否拒绝 | 预期层返回稳定失败类别 | 不应公开可操作的绕过细节 |
| 重验触发器 | 哪些变化会让旧证据失效 | 应用、策略、模型或签名变化即重验 | 旧版本结论不能迁移到新候选 |
事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| Apple App Attest 的应用证明与断言需要由服务器验证,并关联到服务器关心的具体请求。 | Apple App Attest server validation | 该平台材料不证明用户拥有模型权益,也不保证设备或进程在所有时刻绝对可信。 |
| Play Integrity 标准请求可使用 requestHash 将完整性响应绑定到稳定序列化后的业务请求,并由后端重算核对。 | Play Integrity standard requests | requestHash 的正确性依赖双方采用同一规范化输入,完整性响应仍需结合业务授权规则。 |
| Play Integrity 应作为整体反滥用策略中的一个信号,并靠近受保护操作由后端完成验证和决策。 | Play Integrity overview | 平台信号不证明 VMP 配置、Root 状态、模型保密性或任何攻击已经被阻断。 |
| DPoP 可以将 OAuth 访问令牌绑定到客户端持有的密钥,降低被盗令牌在其他客户端重放的风险。 | RFC 9449 OAuth DPoP | DPoP 不证明设备可信,也不能替代短有效期、受众、scope、撤销与最终业务授权。 |
| OAuth 部署需要系统处理令牌重放、授权码注入、重定向和不安全授权模式等已知风险。 | RFC 9700 OAuth Security BCP | 该最佳实践不决定某个 AI 服务的套餐、模型版本、次数或地区权益。 |
| 更新客户端可以通过签名元数据、版本、过期时间和一致性关系识别回滚、冻结与混搭风险。 | The Update Framework specification | TUF 解决更新信任链问题,不直接定义移动端模型的商业权限或平台证明流程。 |
| 工程判断:挑战、证明、会话、权益和模型版本应共享可核对的请求标识,并在服务端以原子状态转换完成放行。 | 工程判断:基于跨服务请求绑定、并发消费和最小授权原则 | 具体字段、存储和有效窗口需要按项目架构与风险等级设计,不能照搬为通用合规结论。 |
| 当前没有具体候选包、真实平台证明回执、权益数据库与资源服务事件链,不能宣称模型重放或越权已经被阻断。 | 项目证据尚未接入 | 文章提供的是架构、代码检查器和验收方法,不包含客户案例、性能数字或产品实测结论。 |
工程常见问题
平台证明通过后,为什么还不能直接下发端侧模型?
平台证明主要提供应用实例和运行环境的风险信号,不回答当前用户是否购买模型、是否允许访问指定版本以及本次操作是否在有效范围。服务端仍需核对会话主体、模型权益、动作、受众、期限和产物身份,并把这些条件绑定到同一请求。
一次成功的 App Attest 或 Play Integrity 结果可以缓存多久?
没有适用于所有业务的固定时长。缓存窗口应按模型价值、操作风险、平台信号和可用性目标决定。高价值模型下载更适合让证明靠近当前请求;即使允许短期缓存,也要绑定应用实例、会话、模型动作与策略版本,撤销事件应优先于缓存。
DPoP 能否代替 App Attest 或 Play Integrity?
不能。DPoP 证明调用方持有与访问令牌绑定的密钥,重点降低令牌被复制后的重放;平台证明提供应用实例和平台环境相关信号。两者覆盖的威胁不同,还都不能替代用户会话、模型权益和资源服务的最终授权。
模型已经下载到设备后,服务端撤销权益还有意义吗?
有意义,但边界必须说清。撤销可以阻止后续下载、密钥获取、在线推理或周期性续期,却不能自动抹除设备上已经存在的明文或运行时数据。离线可用窗口越长,撤销生效越慢,业务需要显式接受并记录这部分剩余风险。
为什么模型授权必须精确到版本和摘要?
同名模型可能因量化方式、分词器、适配器或依赖变化成为不同产物。只授权产品名会让旧令牌覆盖未审核的新文件,也难以识别回滚与混搭。权益令牌、更新元数据和资源路径使用同一模型标识、版本与摘要,才能把商业权限落到具体交付对象。
怎样证明绑定流程已经正确实现?
需要固定候选身份和策略版本,保存挑战签发、平台验证、会话核对、权益匹配、产物确认、令牌签发与资源服务接受的连续事件,并执行请求标识替换、模型版本替换、错误受众、已消费挑战、过期与撤销等负向用例。只有真实回执闭合后才能登记项目通过。