防止 BLE 时间被伪造

为什么目前主人并不负责授时

在采集控制器日志的 6.12 版本里,Android 代码中根本没有这样的机制——不存在来自主人手机的后台授时。时间只是主人 fire(第 3 阶段)的副产品,而且还到不了:继电器一动,控制器立刻断开连接。

6.13 增加了独立的后台连接:每次被动扫描到控制器,主人的手机就开一条自己的 GATT 会话写入 TIME。但时间来源依然只有主人一个。

任何受信任的客户端都应能授时

否则局面就是:主人在度假,控制器断电,重启后 rtc=0——访客只能等到主人回来。不能这么设计。

应允许持有有效令牌的访客用自己的访客密钥签署 TIME。

如何杜绝伪造——单调棘轮,仅对访客生效

思路很简单: 对访客而言,时间只能向前走。主人是可信的,可以不受限制地双向调整——万一有人误设成 2099 年,就得能回退。

控制器把最后已知时间存在 NVS 里(time_ratchet)。访客写入 TIME 时:

主人写入 TIME 时,棘轮直接被其值覆盖,方向不限。

重启后控制器从 rtc = ratchet 起步,而不是从 0。RTC 由此继续计时。恢复出厂设置也会清除棘轮,但配对会立刻把当前时间写进去。

为什么这能挡住恶意访客

发起攻击的访客想延长一个 valid_until = X)的令牌。他能试些什么?

方案 1——把时间往回拨 (把时钟设成昨天,让令牌看着还新鲜):
→ 棘轮不允许。拒绝。

方案 2——把时间往前拨 (把时钟拨快一年):
→ 控制器接受,因为时间确实向前走了。
→ 但控制器此刻比较的是 valid_until=X ,带 now = a year aheadX < now → 于是他自己的令牌已经过期。
→ 攻击者只伤到了自己。

访客唯一诚实的做法就是设置真实的当前时间。任何伪造要么被拒,要么反噬自身。

谁来签署 TIME

控制器应接受由以下密钥签署的 TIME 写入:

TIME 载荷中增加两个字节: bleId。随后控制器依次尝试:

  1. 用 owner_secret 计算 HMAC——对得上就接受。
  2. 否则为该 bleId 派生 guest_key 再验一次。

没有经服务器签发的合法访客捆绑包就无法签署 TIME——攻击者没有密钥。

额外防护:新鲜窗口内的“先 fire”闸门

如果上一次成功同步是在 不到一小时前,控制器就认为自己的时钟是新鲜的,没理由无缘无故更新。在该窗口内,只有客户端在过去 10 秒内完成过一次成功的 FIRE,TIME 写入才会被接受——这证明它持有未过期的有效令牌。

如果已经过了 一小时以上 fire 闸门关闭,TIME 可自由接受,访客仍受 +24 小时上限约束。

这能堵住什么:持有被盗但 已过期 访客捆绑包的攻击者无计可施。他的 FIRE 会因超期被拒,因此够不到新鲜窗口内的 TIME 写入。窗口之外的偏移依然被限制在 24 小时内,主人下次来时会把时间调正。

附带好处:没有冲突

若主人和三位访客同时写入不同时间,每次写入要么因更大而推进棘轮,要么因更小而被拒。这里既无争用也无竞态。

需要做什么

控制器固件:

Android:

此后:

以 NTP 为主要来源(可选)

若控制器处于 Wi-Fi 覆盖范围内,更稳妥的做法是把 BLE 授时彻底移出关键路径,直接从 NTP 服务器取时间。

固件实现(NTP_ENABLED=1):

NTP 与深度睡眠共存

用干电池供电时,控制器大部分时间在睡眠,无法维持 Wi-Fi。办法是:普通唤醒只跑 BLE,窗口约 2.5 秒;而每第 DEEP_SLEEP_NTP_EVERY_N_WAKES次唤醒延长到 20 秒,启动 Wi-Fi 并走 SNTP。当 wake=3 秒、N=7200 时,大约每 6 小时同步一次 NTP,能耗仍在可接受范围。

固件中的可配置常量

时间策略、睡眠与 NTP 的所有参数都在草图开头的 CONFIG 块里,可按现场调整: