Skip to content

ntpstats.roughtime

Roughtime client (draft-ietf-ntp-roughtime-19).

Roughtime gives a signed, coarse (about 1 s) time from several servers and makes a lying server provable: every request nonce is derived from the previous response, so a chain of responses shows the order in which they were received. If two responses cannot both be right, the chain is a malfeasance report anyone can verify.

For ntpstats this is an independent, authenticated sanity bound: an NTP or PTP clock that disagrees with Roughtime by more than the Roughtime radius is wrong, whatever its own statistics say.

Signature checks need cryptography (pip install 'ntpstats[nts]').

RoughtimeError

Bases: Exception

Malformed, unverifiable or missing response.

Response dataclass

A verified Roughtime response and the local times around it.

offset property

Server - local, at the local midpoint (reference - local, like the rest of ntpstats).

bound property

|true offset - offset| <= radius + rtt/2.

Server dataclass

parse(spec, known=None) classmethod

NAME (from the list), or host:port=BASE64KEY.

Measurement dataclass

local_bound()

Interval for (true - local) that every response allows, or None if they do not all overlap.

malfeasance_report()

draft-19 section 8.4.1 report: the chain of requests and responses, in order.

H(data, size=32)

SHA-512 truncated to size bytes (32 in the IETF drafts).

encode(msg)

Roughtime message: N, N-1 offsets, N tags (sorted as uint32 LE), values.

build_request(nonce, public_key=None, versions=DEFAULT_VERSIONS, min_size=MIN_REQUEST)

Request packet: VER, NONC, TYPE=0, SRV (when the key is known), ZZZZ padding.

build_google_request(nonce, min_size=MIN_REQUEST)

Google-Roughtime request: unframed message with a 64-byte NONC and PAD\xff padding to min_size.

merkle_root(leaf, path, index, size=32)

Root from a leaf hash, PATH and INDX (draft-19 section 5.3.1); None if INDX has extra bits.

verify(request, response, public_key, server='', t_send=0.0, t_recv=0.0)

Check a response against its request and the server's long-term key (draft-19 section 5.4).

Raises :class:RoughtimeError on any failure. Older drafts that sign the nonce instead of the request, and microsecond timestamps, are accepted.

load_servers(source=None)

Servers from a draft-19 server list (JSON, section 8.3), or the bundled list.

backoff(n, base=1.0)

Minimum wait before retry n (1-based): min(base * 1.5**(n-1), 86400) s (draft-19 section 5).

query(server, nonce=None, timeout=3.0, tcp=False, retries=0, versions=DEFAULT_VERSIONS, family=0, sleep=time.sleep, google_fallback=True, nonce64=None)

One verified exchange. UDP by default; tcp=True for paths that drop large datagrams.

If the IETF request gets no answer, a Google-Roughtime request (the pre-IETF protocol that some servers still answer, with nonce64) is tried once over UDP; :attr:Response.protocol says which one answered.

causal_violations(responses)

Pairs (i, j), i received before j, with MIDP_i - RADI_i > MIDP_j + RADI_j.

measure(servers, rounds=2, timeout=3.0, tcp=False, spacing=0.0, rand=os.urandom, query_fn=None, **kw)

Query the servers in order, rounds times (two per draft-19 section 8.2), chaining nonces.

Each nonce after the first is H(previous response || rand), so the responses prove their order (for a Google-Roughtime fallback the full 64-byte SHA-512 of the same input). Failed servers are recorded and skipped.

verify_report(report)

Re-check a malfeasance report: every response valid, and each nonce chained from the previous one.