4、认领协议设计:挑战-响应认证协议、基于Token的认领流程、时间戳防重放攻击
设备认领,说白了就是证明「这东西是你的」。但物联网设备不像手机,没有屏幕让你输密码,也没有指纹识别。那怎么证明?靠协议。
我早年做智能门锁项目时,就踩过一个大坑——设备刚出厂,谁都能发个「认领」指令,结果第一批测试锁全被隔壁实验室的同事认领走了。从那以后,我对认领协议的设计就格外较真。
这一章,我们重点拆解三个核心机制:挑战-响应认证、基于Token的认领流程、时间戳防重放攻击。它们组合起来,就是一套工业级的设备认领方案。
4.1 挑战-响应认证协议
先问一个问题:设备没有用户界面,怎么证明「我就是我」?
传统做法是预置一个密钥。设备出厂时烧录一个唯一密钥(比如设备密钥 Device Secret),服务器也知道这个密钥。但问题是,如果直接把这个密钥当密码传,抓包就能拿到,等于白给。
挑战-响应(Challenge-Response)就是来解决这个问题的。它的核心逻辑是:我不直接告诉你密码,但我能证明我知道密码。
挑战-响应流程(简化版)
- 设备发起认领请求,携带设备ID。
- 服务器生成一个随机数(Challenge),返回给设备。
- 设备用预置密钥对Challenge进行签名(HMAC或对称加密),生成Response。
- 服务器用同样的密钥验证Response。匹配,则认证通过。
这里的关键是:Challenge每次都不一样。攻击者就算截获了某次通信,也无法重放,因为下次Challenge变了。
我个人习惯用 HMAC-SHA256 来做签名。为什么?因为HMAC运算快,适合资源受限的MCU,而且安全性足够。我在一个温湿度传感器项目里,用ESP8266跑HMAC-SHA256,每次计算不到2ms,完全没压力。
避坑指南
我曾经遇到过Challenge长度太短的问题。当时用了4字节随机数,结果生产环境出现Challenge碰撞,两个设备拿到了相同的Challenge,导致认证异常。后来统一改成16字节,再没出过问题。记住:Challenge至少16字节,用真随机数生成器。
4.2 基于Token的认领流程
挑战-响应解决了「设备身份验证」,但认领不只是验证身份,还要建立所有权关系。这就需要一个Token机制。
Token是什么?你可以把它理解成一张「临时通行证」。设备完成挑战-响应后,服务器颁发一个Token,设备后续的所有操作都靠这个Token来证明「我已经被认领了」。
我设计的认领流程一般分三个阶段:
| 阶段 | 动作 | 说明 |
|---|---|---|
| 1. 预认领 | 用户App扫描设备二维码,获取设备ID和公钥 | 二维码包含设备ID + 一次性公钥(或证书) |
| 2. 挑战-响应 | App发起认领请求,设备与服务器完成挑战-响应 | 验证设备身份,防止伪造设备 |
| 3. Token颁发 | 服务器生成认领Token,返回给设备和App | Token绑定设备ID + 用户ID + 时间戳 |
Token本身需要包含哪些信息?我建议至少包含:
- 设备ID:明确归属哪个设备
- 用户ID:明确归属哪个用户
- 时间戳:用于判断有效期
- 签名:防止Token被篡改
Token的格式,我个人偏爱 JWT(JSON Web Token)。结构清晰,解析方便,而且生态成熟。你想想看,一个JWT Token长这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJkZXZpY2VfaWQiOiJkZXYtMDAxIiwidXNlcl9pZCI6InVzZXItMTIzIiwiaWF0IjoxNzAwMDAwMDAwLCJleHAiOjE3MDAwMDM2MDB9.
s9f3c8a2b1d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0
嗯,这里要注意:Token一定要有过期时间。我见过有人把Token设成永久有效,结果设备转卖后,原用户还能控制设备,这就出大问题了。建议认领Token有效期设为24小时,设备后续通信使用短期Session Token。
重要提醒
Token在传输过程中必须使用TLS加密。我曾经在内部测试环境偷懒没开TLS,结果Token被中间人截获,攻击者直接拿着Token冒充设备。从那以后,我所有设备通信都强制走TLS,哪怕是在局域网内。
4.3 时间戳防重放攻击
挑战-响应已经能防重放了,为什么还要时间戳?
因为挑战-响应只防「请求重放」,但防不了「Token重放」。攻击者截获了一个合法Token,在有效期内可以反复使用。怎么办?加时间戳。
时间戳防重放的原理很简单:每条消息都带上当前时间,服务器只接受时间差在允许范围内的消息。
具体做法:
- 设备发送请求时,在消息体或Header里附加
timestamp字段。 - 服务器收到后,计算
|当前时间 - timestamp|。 - 如果差值超过阈值(比如5秒),直接拒绝。
这里有个细节:设备时钟可能不准。物联网设备很多没有RTC,或者RTC电池没电了,时间可能差几个小时。怎么办?
我的做法是:设备在认领时同步一次服务器时间。设备发起挑战-响应时,服务器在Challenge响应里附带当前时间戳。设备用这个时间校准本地时钟,后续通信都基于这个校准时间。
经验之谈
我曾经遇到一个坑:设备在校准时间后,本地时钟漂移太快。一个低成本的蓝牙芯片,每小时能漂移好几秒。后来我加了一个「时间漂移补偿」机制——每次通信时,设备记录服务器返回的时间,与本地时间做差值,动态调整。说白了,就是每次通信都顺便校时一次。
时间戳防重放还有一个好处:可以配合Nonce(一次性随机数)一起用。我习惯在消息里同时放 timestamp 和 nonce,服务器维护一个最近使用过的Nonce列表。这样即使攻击者在同一秒内重放,也会因为Nonce重复而被拒绝。
4.4 完整认领协议流程
把上面三个机制串起来,就是一套完整的认领协议。我画了一张流程图,方便你理解整体逻辑:
整个流程走下来,你会发现:挑战-响应保证了设备身份的真实性,Token建立了所有权关系,时间戳和Nonce杜绝了重放攻击。三者缺一不可。
4.5 代码示例:挑战-响应核心逻辑
最后,给一段实际可用的伪代码。这是设备端的HMAC计算逻辑:
// 设备端:计算挑战-响应
function handleChallenge(challenge, deviceSecret) {
// 1. 拼接Challenge + 设备ID + 时间戳
const message = challenge + "|" + deviceId + "|" + currentTimestamp;
// 2. 使用HMAC-SHA256计算签名
const response = hmacSha256(deviceSecret, message);
// 3. 返回Response + 时间戳 + Nonce
return {
response: response,
timestamp: currentTimestamp,
nonce: generateRandomNonce()
};
}
// 服务器端:验证挑战-响应
function verifyResponse(deviceId, challenge, response, timestamp, nonce) {
// 1. 检查时间戳是否在允许范围内(±5秒)
if (Math.abs(Date.now() - timestamp) > 5000) {
return false; // 时间戳超限,拒绝
}
// 2. 检查Nonce是否已使用(防重放)
if (nonceUsedSet.contains(nonce)) {
return false; // Nonce重复,拒绝
}
nonceUsedSet.add(nonce);
// 3. 从数据库获取设备密钥
const deviceSecret = getDeviceSecret(deviceId);
// 4. 重新计算期望的Response
const message = challenge + "|" + deviceId + "|" + timestamp;
const expectedResponse = hmacSha256(deviceSecret, message);
// 5. 比对
return response === expectedResponse;
}
安全红线
设备密钥(Device Secret)绝对不能硬编码在App里。我见过有人把密钥写在App的配置文件里,结果反编译后密钥全泄露。正确的做法是:密钥只存储在设备安全区(如TEE或Secure Element)和服务器数据库,App只做转发,不接触密钥。
好了,这一章的内容就这些。挑战-响应、Token、时间戳,这三个东西组合起来,就是一套能打硬仗的认领协议。你可以在自己的项目里直接套用这个框架,根据设备算力和安全等级做适当裁剪。