NTS: Authenticated Time at Meta
Facebook Engineering

NTS: Authenticated Time at Meta

  • Meta’s public time service now speaks NTS (Network Time Security, RFC 8915) at nts.meta.com. Packets are authenticated, so a device can verify the time came from us and was not modified on the way. - Our NTS servers hold no per-client state. Cookie keys are derived, not stored and not replicated. - We’ve open sourced everything, including the protocol, server, client through Meta’s Time library on GitHub. - We’re also encouraging everyone, particularly those who maintain an NTP client on Android or iOS, to join us in enabling NTS support. We’ve previously written about building a more accurate time service at Meta scale - migrating from ntpd to chrony, going from 10 milliseconds to 100 microseconds, and opening time.meta.com to everyone. That work made time precise. This work makes it verifiable. Why NTS? NTP has been unauthenticated since 1985, like a lot of the internet’s foundational protocols. But it is also the one every certificate, token, and signature is checked against. The client sends 48 bytes, something sends 48 bytes back, and the client believes it. There is no signature and no identity. It is not alone in that. Plenty of the internet’s foundational protocols were designed for a network where nobody was assumed to be hostile, and the industry has been retrofitting them ever since. What makes time different is that it is not just another protocol to secure. It is the input every other security decision is measured against. For most of NTP’s life that did not matter much, because a clock being a few seconds off was an operational annoyance. Today it is load-bearing: - Certificate validation: notBefore and notAfter are compared against the local clock. Wrong clock, wrong answer. - Token and credential expiry: “Valid for 15 minutes” is a claim about a clock. - Replay windows: Rejecting a request older than N seconds requires knowing what time it is. - Log correlation and ordering: Two hosts that disagree produce a timeline that never happened. And the check itself is circular. You can’t do X.509 notBefore/notAfter validation without knowing the current time. Effectively you’d have to disable cert time validation, or implement one of the suggested work-arounds. There are two common workarounds. Both are bad. You can skip validation until the clock is set, which leaves a device unprotected exactly when it is most exposed. Or you can build in-slack: A certificate issued at 12:00:00 is rejected by a client whose clock reads 11:59:30, so issuers backdate notBefore by a few minutes and validators quietly widen the window at their end. Both sides are guessing at how wrong the other one is. That was survivable when a certificate lasted 398 days. CA/Browser Forum ballot SC-081v3 changed that. The cap on newly issued publicly trusted TLS certificates dropped to 200 days in March 2026, falls to 100 days in March 2027, and reaches 47 days in March 2029 - with domain validation reuse dropping to 10 days at the same point. At 47 days, manual certificate management stops being viable at any scale. Renewal moves to roughly monthly, and it happens unattended - usually over ACME. An automated renewal loop on a host with a skewed clock does not raise a ticket. It reissues, or it fails, on a schedule nobody is watching. How NTS Works NTS works in phases, and separating them is what makes it deployable. Phase 1 - Key Establishment (NTS-KE): TLS 1.3 over TCP/4460, negotiated with ALPN ntske/1. The client and server agree on an AEAD algorithm, then derive two directional session keys (C2S and S2C) straight from the TLS session using the RFC 5705 exporter. The server issues eight cookies, tells the client which NTP server to actually use via a server negotiation record, and closes the connection. This happens once, not per packet. We negotiate three AEADs: AES-SIV-CMAC-256 (ID 15), which RFC 8915 makes mandatory to implement and is what an unknown client is most likely to ask for; AES-SIV-CMAC-512 (17); and AES-128-GCM-SIV (30). Client preference wins. Phase 2 - Authenticated NTP: Standard NTPv4 over UDP/123 to the advertised server, with NTS extension fields. A request carries a unique identifier, a cookie, and an authenticator; the authenticator covers everything before it, but encrypts nothing. The server opens the cookie to recover the session keys, verifies that authenticator, and replies with the unique identifier echoed back plus an authenticator of its own - and that one does carry a payload: the fresh cookies, encrypted inside it. A forged packet fails verification and is dropped. We do not send an NTS NAK, so a failure is indistinguishable from packet loss and cannot be used to push a client into a fresh key exchange. A replayed response carries the wrong unique identifier and matches no outstanding request. Because the replacement cookies ride inside the response authenticator rather than in the clear, an observer sees a different opaque blob every exchange and cannot follow a client between networks. Spending each cookie once is the client’s side of that bargain - the server is stateless and does not track them. Cookies Without State A cookie is a server state handed to the client for safekeeping. It holds the session keys, sealed with AES-SIV so only the server can open it. The obvious implementation is a key ring replicated to every server, which turns time service into a distributed state problem. We do not replicate keys. Each server derives the sealing key from a shared master secret and the current day: | key = HKDF-SHA256(master, salt = BE32(day), info = “fbnts-cookie-seal-v1”) | Since the Unix epoch, day is measured as whole 24-hour periods, not a calendar day. So there is no timezone and no DST, and every host computes the same integer for the same instant. That integer is the cookie’s key ID and travels in the clear. A server that has been handed a cookie it has never seen or sealed by a host it has never talked to, re-derives the key and opens it. We accept two days back and one forward to cover skew around a rotation boundary. The result is no key ring, no replication, no session table, no shared state to fall out of sync, and nothing for an attacker to exhaust. It is also what makes the split above work - the KE server and the NTP servers it hands you off to are different machines in different places, and they never exchange anything. Adding a server is adding a server. The plain NTP path is untouched. The responder parses extension fields only when they are present, so unauthenticated clients hit the same code as before at the same cost. Using NTS One line of chrony config: | pool nts.meta.com nts iburst maxsources 5 | nts.meta.com terminates key exchange only. During the handshake it advertises an NTP responder in a server negotiation record, so the client is steered to time.meta.com and sends its authenticated NTP there. The host you configure is not the host you end up talking to. That indirection is why we use pool rather than server. A single server line gives you one association and one responder, which is a single point of failure - and NTP is a protocol that wants several sources so it can outvote a bad one. pool asks for maxsources independent associations. Each runs its own key exchange over its own TLS session, so each ends up with a distinct pair of session keys: | 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 | Five rows, five independent authenticated sources, from one line of config and one KE endpoint. Reading across: Type 30 is the negotiated AEAD, AES-128-GCM-SIV, with KLen 128 bits of session key - chrony lists it first and client preference wins. CLen 68 is the cookie: 36 bytes of envelope around the two 16-byte keys inside. Cook 8 is cookies in hand and NAK 0 means none has been rejected. The more interesting half is not in the table. Each row’s session keys came out of its own TLS handshake and work only for that association. The key that seals them into a cookie is the shared one - the epoch day number from the derivation above, identical on every responder we run, stamped inside each cookie and never visible to the client. That split is what lets a single key exchange hand a client five different responders that have never spoken to each other. When a pool member completes key exchange and moves to its negotiated responder, the freed KE address is handed to the next unresolved member, repeating until maxsources responders have answered. Five sources come up in about 20 minutes as each member completes its own key exchange in turn. NTS Protects Against Attacks Unlike NTP, NTS stops a man-in-the-middle (MITM) attack from forging the answer. A classic MITM attack doesn’t need to break any cryptography, it just has to answer a client before a real server does. If an attacker rolls the clock backward they can make expired certificates validate again or revoked tokens work again. Any control that relies on something having expired is reset. If they roll the clock forward everything expires at once. This is the more difficult direction to address because beyond a certain point the failure stops being recoverable. Let’s look at an example of what happens to a device that gets convinced it is the year 2037. NTP counts seconds in a 32-bit field that wraps in February 2036, and a client stranded on the far side of that boundary computes its correction into the wrong era - so the mechanism that exists to fix a wrong clock now moves it further away. Meanwhile every certificate the device holds is long expired, so TLS fails, therefore it cannot reach anything that might help, including its own update
Read on Facebook Engineering ↗ ← Back to News

Comments

No comments yet. Start the discussion.