Meta 部署 NTS:实现经过认证的时间服务
- 数据包经过认证,设备可以确认时间确实来自我们,且途中未被篡改。
- 我们的 NTS 服务器不保存任何客户端状态。Cookie 密钥是推导出来的,既不存储,也不复制。
- 我们也呼吁所有人加入我们启用 NTS 支持,尤其是那些在 Android 或 iOS 上维护 NTP 客户端的开发者。
我们之前写过一篇关于在 Meta 规模下构建更精确时间服务的文章——从 ntpd 迁移到 chrony,把误差从 10 毫秒降到 100 微秒,并向所有人开放了 time.meta.com。
那一步让时间变得精确,这一步让时间变得可验证。
为什么选择 NTS?
和许多互联网基础协议一样,NTP 自 1985 年起就一直没有认证机制。但偏偏所有证书、令牌和签名都要靠它来校验。客户端发出 48 字节,对方回 48 字节,客户端就信了。没有签名,也没有身份验证。
NTP 并非孤例。互联网的许多基础协议在设计之初都假设网络中没有恶意者,行业此后一直在给它们打补丁。但时间的特殊之处在于,它不只是又一个需要加固的协议,而是所有其他安全决策所依赖的基准输入。
在 NTP 的漫长历史中,这不是什么大问题——时钟偏差几秒顶多算运维上的小麻烦。但今天,时间已经成了承重墙:
- 证书校验:notBefore 和 notAfter 都要与本地时钟比对。时钟错了,结论就错了。
- 令牌和凭证过期:"有效期 15 分钟"本质上是对时钟的一个断言。
- 防重放窗口:要拒绝超过 N 秒的旧请求,前提是知道现在几点。
- 日志关联与排序:两台时间不一致的主机,会产生一条从未真实发生的时间线。
而且这个校验本身是循环依赖的:不知道当前时间,就无法做 X.509 的 notBefore/notAfter 校验。实际上你只能禁用证书时间校验,或者采用某种变通方案。
常见绕行方案有两种,但都不可取。一种是等到时钟校准完成前跳过校验,这会让设备在最脆弱的时刻处于无防护状态;另一种是引入时间余量(slack):证书若在 12:00:00 签发,时钟显示 11:59:30 的客户端就会拒绝它,因此签发方会将 notBefore 时间向前回溯几分钟,校验方则默默放宽校验窗口。双方都在盲目猜测对方的时钟偏差有多大。
当证书有效期长达 398 天时,这种做法还能勉强应付。CA/Browser Forum 的 SC-081v3 决议 改变了这一局面。新签发的公有信任 TLS 证书有效期上限将在 2026 年 3 月降至 200 天,2027 年 3 月降至 100 天,2029 年 3 月进一步缩短至 47 天,届时域名验证(Domain Validation)的重用期限也将同步降至 10 天。
当有效期仅剩 47 天时,在任何规模的场景下,手动管理证书都将不再现实。更新频率接近每月一次,且完全无人值守——通常通过 ACME 协议自动完成。一个时钟失准的主机上的自动续订循环不会主动报警,它只会按照无人监管的日程静默地重新签发证书或导致失败。
NTS 的工作原理
NTS 分阶段执行,各阶段解耦正是其可部署性的关键。
第一阶段——密钥建立(NTS-KE):基于 TCP/4460 端口的 TLS 1.3,通过 ALPN 协商 ntske/1 扩展。客户端与服务器商定一种 AEAD 算法,随后直接利用 RFC 5705 导出器从 TLS 会话中派生出两个方向性的会话密钥(C2S 和 S2C)。服务器签发八枚 Cookie,通过服务器协商记录告知客户端应实际使用的 NTP 服务器,然后关闭连接。这一阶段仅执行一次,并非针对每个数据包。
系统协商三种 AEAD 算法:AES-SIV-CMAC-256(ID 15,RFC 8915 强制要求实现,也是未知客户端最可能请求的算法)、AES-SIV-CMAC-512(ID 17)以及 AES-128-GCM-SIV(ID 30)。客户端的偏好优先。
第二阶段——认证 NTP:向广播地址发起标准的 NTPv4 请求,经由 UDP/123 端口发送至通告的服务器,并携带 NTS 扩展字段。请求中包含唯一标识符、Cookie 和认证器;认证器覆盖其之前的所有字段,但不加密任何数据。服务器打开 Cookie 以恢复会话密钥,验证该认证器,随后回复一个包含回显的唯一标识符及其自身生成的认证器——而这个认证器内嵌了负载:新签发的 Cookie 被加密在其中。
伪造的数据包验证失败后会被直接丢弃。我们不会发送 NTS NAK,因此失败与丢包在表现上无法区分,攻击者无法借此逼迫客户端发起新的密钥交换。重放响应携带了错误的唯一标识符,无法匹配任何挂起请求。由于替换 Cookie 嵌在响应的认证器内部而非明文传输,观察者在每次交互中看到的都是不同的不透明数据块,无法追踪客户端跨网络的行为。一次性使用 Cookie 是客户端在这笔交易中的责任——服务器端无状态,不会追踪它们。
无状态的 Cookie
Cookie 是服务器交给客户端保管的状态数据。它保存了会话密钥,使用 AES-SIV 密封,只有服务器能够解开。常见的实现方式是将密钥环复制到每台服务器上,但这会把时间服务变成一个分布式状态问题。
我们不复制密钥。每台服务器基于共享的主密钥和当前日期来派生密封密钥:
| key = HKDF-SHA256(master, salt = BE32(day), info = “fbnts-cookie-seal-v1”) |
从 Unix 纪元开始,day 以完整的 24 小时周期计算,而非日历日。因此不存在时区和夏令时问题,所有主机在相同时刻计算出的整数都一致。这个整数就是 Cookie 的密钥 ID,以明文传输。当服务器收到从未见过或来自从未通信过的主机的 Cookie 时,会重新派生密钥并解开它。为覆盖密钥轮换边界附近的时钟偏差,我们接受前两天和后一天。
结果是:没有密钥环、没有复制、没有会话表、没有可能不同步的共享状态,攻击者也无可利用的资源进行耗尽攻击。这也是上述分离架构得以运作的原因——KE 服务器和你随后连接的 NTP 服务器是位于不同地点的不同机器,它们之间从不交换任何信息。增加一台服务器就是增加一台服务器。
普通 NTP 路径未受影响。响应方仅当扩展字段存在时才会解析它们,因此未认证的客户端访问的是与此前相同的代码路径,开销相同。
使用 NTS
只需在 chrony 配置中加一行:
| pool nts.meta.com nts iburst maxsources 5 |
nts.meta.com 只负责密钥交换。在握手过程中,它会通过 server negotiation 记录告知客户端 NTP 响应器的地址,于是客户端被引导到 time.meta.com,并把认证后的 NTP 请求发往那里。你配置的主机,并不是你最终实际通信的主机。
正因为这种间接机制,我们用 pool 而不是 server。一条 server 配置只会建立一个 association、对应一个响应器,等于单点故障——而 NTP 本来就需要多个时间源,才能通过多数票淘汰出错的那一个。pool 会请求 maxsources 个独立的 association,每个都在各自的 TLS 会话上独立完成密钥交换,因此各拿到一对不同的会话密钥:
| Name/IP address | Mode | KeyID | Type | KLen | Last | Atmp | NAK | Cook | CLen |
|---|---|---|---|---|---|---|---|---|---|
| time1.meta.com | NTS | 5 | 30 | 128 | 27 | 0 | 0 | 8 | 68 |
| time2.meta.com | NTS | 7 | 30 | 128 | 142 | 0 | 0 | 8 | 68 |
| time3.meta.com | NTS | 10 | 30 | 128 | 548 | 0 | 0 | 8 | 68 |
| time4.meta.com | NTS | 1 | 30 | 128 | 1177 | 0 | 0 | 8 | 68 |
| time5.meta.com | NTS | 2 | 30 | 128 | 1177 | 0 | 0 | 8 | 68 |
五行就是五个相互独立的认证时间源,而这一切只来自一行配置、一个 KE 端点。
逐列来看:Type 30 是协商出的 AEAD 算法 AES-128-GCM-SIV,KLen 128 表示 128 位的会话密钥——chrony 把它排在首位,且客户端偏好优先。CLen 68 是 cookie 的长度:两个 16 字节的密钥外面包了 36 字节的信封。Cook 8 表示手头有 8 个 cookie,NAK 0 表示没有 cookie 被拒绝过。
真正有意思的部分不在表里。每一行的会话密钥都源自各自独立的 TLS 握手,且仅对该关联连接有效。将这些密钥封装进 Cookie 的,是共享密钥——即上述推导中的纪元天数值,我们在每台应答器上都使用相同的值,并将其戳记在每个 Cookie 内部,客户端永远看不到。正是这种拆分,使得一次密钥交换就能让客户端对接到五个彼此从未通信过的不同应答器。
当池中的一个成员完成密钥交换并切换到协商好的应答器后,释放出的 KE 地址会交给下一个未解决的成员,如此循环,直到 maxsources 数量的应答器做出响应。每个成员依次完成自身的密钥交换,五路源大约需要 20 分钟建立完毕。
NTS 抵御攻击
与 NTP 不同,NTS 能阻止中间人(MITM)攻击者伪造应答。经典的 MITM 攻击甚至不需要破解任何密码学,只需抢在真实服务器之前回答客户端即可。如果攻击者将时钟回拨,就能让过期的证书重新通过验证,或让被吊销的令牌再次生效。任何依赖“过期”机制的控制手段都会被重置。如果攻击者将时钟快进,则所有有效期会同时失效。这个方向更难处理,因为一旦超过某个临界点,故障就无法恢复。
看一个例子:一台设备被欺骗,认为现在是 2037 年。NTP 使用 32 位字段计数秒数,该字段会在 2036 年 2 月发生回绕。卡在边界另一侧的客户端会计算出错误年代的修正值——本用于纠正错误时钟的机制,反而让它偏离得更远。与此同时,设备持有的所有证书早已过期,TLS 握手失败,导致它无法访问任何可能提供帮助的服务,包括自身的更新服务。修复补丁无法送达,因为送达补丁所依赖的正是那个已经错乱的时钟。
设备本身并没有损坏。每个组件都完全按照设计正常运行。它只是持有一个将其置于网络覆盖之外的数值。无论等待多久或重启多少次都无法修复。恢复意味着工厂重置,或送往服务中心。将这个情形放大到整个设备群组,一个错误整数的代价就变成了一个物流问题。
接下来,考虑针对签发凭证的主机发起的时钟前置攻击。由于短生命周期凭证会迅速过期,它们通常被视为安全,短有效期本身起到了凭证吊销的作用。然而,这一特性完全取决于签发主机的时钟。如果签发方被误导认为当前是次年,它所签发的凭证有效期将超出设计的安全窗口。
NTS 无法解决的问题
延迟攻击。RFC 8915 明确指出,攻击者若持有你的数据包,可让你的时钟最多偏移其引入延迟的一半。协议中的任何加密机制都无法检测出这一点,因为所有数据字节都是真实的。身份认证只能告知包是谁生成的,却无法反映它在传输途中耗时多久。缓解措施仍是常规手段,例如使用多个独立时间源、设置合理性限制以及限制步长。
因此,该声明的范围很窄:路径上的攻击者仍然可以延迟或丢弃你的数据包。但他们不再能对内容撒谎。
引导过程。NTS-KE 运行在 TLS 之上,而 TLS 需要一个可用的时钟,这又陷入了与上文相同的循环问题。即使设备的 RTC(实时时钟)已失效,它在首次握手前仍必须获取大致正确的时间。
错误的服务器。NTS 认证的是路径,而非答案。即使一个服务器配置正确、身份认证无误,但如果其提供的时间本身就是错的,它仍会带着完美的签名把错误时间交给你。仍需依靠法定人数机制、合理性检查及监控手段。
单调性。认证时间本质上仍是挂钟时间,而挂钟会倒退——比如在闰秒或任何时间校正发生时。Cloudflare 的权威 DNS 在 2016 年底就遇到了这个问题:跨闰秒测量的解析器往返时间出现了负值,该错误传播至上游选择逻辑,并导致 Go 语言中的 rand.Int63n() 函数 panic。过程中没有任何伪造或误投递发生。如果你的代码需要测量流逝时间,请使用单调时钟。
采用情况
《网络时间协议的网络时间安全》(RFC 8915)于 2020 年 9 月作为 IETF 标准轨道上的拟议标准发布。Cloudflare 早在 15 个月前的 2019 年 6 月就基于当时的草案上线了 time.cloudflare.com 并启用 NTS,证明了其在大规模场景下的可行性。
服务端方面,公开的 NTS 服务列表目前已有几十个条目,主要是各国计量院和互联网基础设施运营商,比如 PTB、Netnod 和 time.nl。现在我们也加入其中。
更大的缺口在客户端。服务器世界里 NTS 支持出现在意料之中的地方——chrony 和 ntpsec 都已实现。但在占互联网终端数量大头的主流平台上,系统自带的时间客户端完全没有这项支持。
Android 的系统时间同步走的是基于 UDP/123 的普通 SNTP——固定 48 字节的数据包,不带扩展字段处理——另外辅以 NITZ 和 GNSS,因此没有任何 NTS 通道。Apple 设备通过 timed 同步 time.apple.com,据我们所知同样不支持 NTS。
这两套系统决定了数十亿部手机、手表和耳机上的时钟,而它们中的每一台都在信任一个未经认证的 UDP 数据包。设备厂商今天就可以内置一个支持 NTS 的独立客户端——chrony 在 Android 上跑得很好——但这终究只是权宜之计,平台本应自己做到。
协议已经定稿,服务器正在陆续上线,客户端实现也已就绪。
缺的就是移动端。
与我们一起启用 NTS
nts.meta.com 已经上线并免费开放。代码托管在 github.com/facebook/time。
如果你运营公开的时间服务,请开启 NTS。如果你维护 NTP 客户端,请实现 RFC 8915。如果你负责移动操作系统的时间栈,缺口就在这里——世界上大多数设备的时钟由 Android 和 iOS 设置,而它们目前仍在使用未经认证的数据包。
我们花了多年时间让时间更精准。但缺乏认证的精准,只是从一个不明来源得到一个很准的数字。现在,两者可以兼得。