核心策略(第二层,现行实现)
第二层访问控制可概括为:
challengeCode:服务端有状态生命周期(Redis TTL)- 请求级校验:时间戳窗口 + HMAC 签名(签名材料包含挑战上下文)
- 短期反重放:Redis
NX去重锁(按请求/按下载分片维度) - 资源访问收敛:通过
authUuid查询下载对象与业务资格(DB 校验)
整体形态属于典型的stateful challenge:挑战码本身不自包含授权语义,关键语义与控制力体现在服务端 Redis 状态中(区别于 JWT 式自包含凭证)。
与第一层的衔接断层(现状)
第一层(TLS/mTLS)与第二层(访问控制)的衔接点体现在“业务侧信任的输入”。目前业务接口主要以X-VIN / X-Signature / X-Timestamp作为鉴权输入,业务代码未直接消费或绑定 mTLS 证书身份。
第一层输出与第二层输入的关系(现状表)
第一层输出 | 第二层输入 | 现状问题/风险点 |
mTLS 验证通过 | 业务侧主体标识仍主要来自 | 风险点:业务侧未显式将“证书身份”与 |
TLS 会话建立 | 通道安全 | 通道安全不等于“请求合法”;现状依赖请求级 |
设备主体识别 |
| “是否具备升级资格”并非仅由 VIN 决定,仍需通过 |
控制器入口层:挑战码签发、下载、版本查询均要求X-VIN / X-Signature / X-Timestamp,入口在OTADownloadSoftwareController。
Challenge Code(生命周期与交互)
设计意图
现状的challengeCode不是替代 JWT 式 token 做“无状态授权”,而是用于构建短期可控的动态授权上下文:
- 服务端签发短期随机值,使后续签名升级为“动态上下文签名”
- Redis TTL 提供可控时效性
- Lua 脚本 +
NX锁将“签发”和“反重放”原子化处理
接口与交互(现状)
现状的挑战签发接口为:
POST /ota/challenge/send- 请求头:
X-VIN、X-Signature、X-Timestamp - 响应体:
challengeCode+status=pending - 入口:
OTADownloadSoftwareController - 实现:
softwareChallengeSend
与“目标演进版”对齐对比(现状没有的):
otp_seedidempotency_key(显式字段)- 权限清单/constraints
挑战码生成与存储(现状)
生成:
%06d六位随机数(伪随机),见softwareChallengeSend- Redis key 前缀:
ota:c:(KEY_CH),见OTADownloadSoftwareServiceImpl
写入:
- 采用 Lua 脚本原子执行(脚本见
RedisLuaConfig)
KEYS[1]:反重放锁 key(示例:ok:s:{signature})KEYS[2]:挑战码 key(ota:c:{deviceUuid})- 锁 TTL:现行传入
120s - 挑战码 TTL:现行传入
3600s
读取:
challengeFetchOnlyScript校验 key 存在并GET,不存在返回-1- 业务侧映射为
CHALLENGE_EXPIRED,见fetchChallengeOnly
下载过程中的挑战码“续期”(现状)
下载接口在“首次下载(非续传 Range)”时会对ota:c:{deviceUuid}延长有效期(当前已调整为2h),逻辑位于downloadSoftwareStorage:
- 续传判定:
range.start > 0认为续传(isSequel) - 非
HEAD且非续传时:expire(KEY_CH + deviceUuid, Duration.ofHours(2))
结论(现状行为边界):
- 下载开始会续期
- 断点续传不会续期
HEAD请求不会触发续期
请求级授权:时间戳窗口 + HMAC 签名 + 反重放(现状)
现状的“请求级授权”由三部分协作完成:时间戳窗口校验、HMAC 签名校验、Redis 短期反重放锁。
时间戳窗口(现状)
- 校验规则:
abs(now - reqTime) <= windowSec - 多个入口
windowSec = 60s - 代码见
isTimestampValid
HMAC 签名校验(现状)
签名消息拼装(Java fallback 逻辑):
parameter != null:message = deviceUuid + ":" + parameter- 否则:
message = deviceUuid - 算法:
HmacSHA256,结果 Base64 - 代码见
verifyHmacSignature
签名实现路径:
NativeSecurity优先,失败回退Java HMAC,见verifyHmacWithFallback
密钥/阶段(现状体现为“双阶段校验风格”):
- challenge 签发阶段:使用
challengeKey,参数timestamp
- 绑定:
deviceUuid + timestamp(见softwareChallengeSend)
- 绑定:
- 后续业务阶段:使用
sharedChallengeKey,参数challengeCode:timestamp
- 绑定:
deviceUuid + challengeCode + timestamp(见downloadInitialVerification、softwareAllVersionInformation)
- 绑定:
反重放 / 幂等(现状)
现状未提供显式idempotency_key字段,依赖 RedisNX锁实现短窗口去重/反重放。锁 key 前缀见LockKey:
ok:s:(challenge send)ok:g:(version info)ok:d:(download)
典型行为(现状):
- challenge send:Lua 脚本中对
ok:s:{signature}做NX + EX(120s),失败报REPLAY_ATTACK_DETECTED,见RedisLuaConfig - version info:
ok:g:{signature},setIfAbsent(..., 2 min),失败判重放,见softwareAllVersionInformation - download:
ok:d:{signature}:{rangePart},setIfAbsent(..., 2 min),失败判重放,见downloadSoftwareStorage
语义边界:该机制实现的是“短窗口去重”,而非“全链路可审计的 jti 消费记录”。
资源访问收敛:authUuid -> 下载对象绑定(现状)
目标演进版强调“动态权限清单(scp)+ constraints + 分片级授权”。现状实现的“最小权限”更偏向以下形态:
- 接口级最小化:关键接口均要求通过
timestamp + signature (+ challengeCode)校验 - 资源级收敛:下载资源不由自包含权限清单描述,而由
authUuid查询并绑定到唯一下载对象(更像“凭证指向资源”)
现状的资源访问关键点在verificationPath(authUuid, remoteIp):
certifiedAuthTokenMapper.getAuthWithBrandByUuid查询认证记录- 校验
brand/project表存在 - 校验下载文件唯一性(必须
size == 1) - 解析下载路径
softwarePath并返回下载上下文(见verificationPath) - 写入 IP 地理信息用于审计/统计,见
remoteIpAddress
现状“最小权限”的准确表述:
authUuid必须指向唯一下载对象,否则拒绝- 关键接口必须处于短期挑战上下文签名之下,否则拒绝
- 尚未形成操作级
scp的细粒度权限体系(如download_chunk/verify/report)
身份到授权的映射(现状)
现状的应用层主体标识来自请求头X-VIN(代码变量名多为deviceUuid),入口集中在 controller 层(如softwareChallengeSend、downloadSoftwareStorage等)。
业务侧对该标识可信性的保障主要依赖:
- HMAC 签名校验(签名输入包含
deviceUuid) - 挑战码上下文(后续接口签名输入包含
challengeCode) - 网关/证书体系对请求来源的约束(该部分不在业务代码中体现)
因此,现状的“身份到授权映射”可写为:
- 第一层:提供通道安全与对端认证(TLS/mTLS)
- 第二层:以
X-VIN为应用层主体标识,结合timestamp + signature + challengeCode建立短期授权上下文 - 资源级授权:通过
authUuid查询并绑定唯一下载对象(softwarePath)
层内收敛与状态依赖(现状表)
机制 | 实现位置 | 状态依赖 | 业务感知 |
mTLS 身份 | 网关/基础设施 | 无 | 无 |
主体标识(VIN/deviceUuid) | 业务 Controller(X-VIN) | 无 | 有 |
时间戳窗口校验 | 业务代码 | 无 | 有 |
HMAC 签名校验 | 业务代码(NativeSecurity/Java fallback) | 无 | 有 |
挑战码签发与有效期 | 业务代码 + Redis Lua | Redis | 有 |
反重放/短幂等 | Redis NX 锁(Lua / setIfAbsent) | Redis | 有 |
下载对象绑定 | DB 查询(authUuid -> softwarePath) | DB | 有 |
“无状态”边界可表述为:
- 密码学验证本身(
timestamp + HMAC)可以无状态 - 授权上下文与反重放显式依赖 Redis 状态
- 业务资格与下载对象绑定显式依赖 DB 状态
因此现状不是“完全无状态授权”,而是“密码学校验偏无状态 + 授权上下文/反重放/资源绑定有状态”的混合模式。
生产特性:弱网 / 续传 / 长下载下的授权恢复(现状)
现状针对 OTA 下载的弱网特点做了部分工程化处理:
- 续传识别:通过
Range判断isSequel,见downloadSoftwareStorage - 重放锁粒度:
signature + rangePart(分片级去重),短 TTL2min,见downloadSoftwareStorage - 长下载的挑战码续期:首次下载(非续传)延长 challenge TTL(你当前为
2h),见downloadSoftwareStorage
当出现“下载时间超过 challenge TTL”:
fetchChallengeOnly直接报CHALLENGE_EXPIRED- 恢复路径通常只能重新走
/ota/challenge/send获取新挑战码
差异点说明:与演进方向相比,现状下载数据面仍主要由业务服务直接提供流式下载,而非对象存储直连的signed URL形态。
层间接口契约(现状版)
输出方 | 输出 | 输入方 | 消费方式 |
第一层(安全防护) | TLS/mTLS 通道(业务侧不可见) | 第二层(访问控制) | 默认信任网络边界/网关;业务侧依赖请求头主体标识与签名链路 |
第二层(访问控制) | challengeCode(Redis 有状态)+ 请求级校验结果 | 下载/版本业务 | 后续请求必须带 X-VIN/X-Timestamp/X-Signature ,并通过 challengeCode 参与的动态验签 |
第二层(访问控制) | 反重放锁(ok:*) | 第二层自身 | 用短 TTL NX 锁拒绝短期重复请求 |
第二层(访问控制) | authUuid -> 下载对象绑定 | 下载模块 | DB 校验通过后获得唯一 softwarePath 并进入下载 |
现状版总结
现状第二层访问控制采用“服务端有状态挑战码 + 请求级 HMAC 校验 + Redis 短期反重放锁”的组合方案。挑战码由服务端签发并存储于 Redis,通过 TTL 控制生命周期;后续关键接口在时间戳窗口校验基础上,将challengeCode:timestamp纳入签名材料实现动态上下文签名;同时通过ok:*系列 RedisNX锁对挑战签发、版本查询与下载分片请求进行短窗口去重以降低重放风险。在资源访问方面,下载对象并非通过自包含权限清单描述,而是通过authUuid查询并绑定到唯一下载路径(softwarePath),从而实现资源级收敛与异常拒绝。该方案强调服务端控制力与工程可落地性,但与目标演进版相比,尚未形成自包含凭证(challengeToken)、操作级权限清单与数据面解耦(对象存储 signed URL)的平台化形态。