An asynchronous implemenation of the Roughtime protocol for Python.
Implements the Roughtime protocol as described in https://datatracker.ietf.org/doc/html/draft-ietf-ntp-roughtime-15.
Draft versions 07 through 15 are supported for querying servers.
Draft versions 10 through 15 are supported for running a server. Also supports queries from Google Roughtime clients.
You can install roughly from GitHub using your favorite package manager, for example with pip:
pip install "git+https://github.com/teaishealthy/roughly.git"
# or with the cli extra
pip install "git+https://github.com/teaishealthy/roughly.git#egg=project[cli]"You can use roughly as a command line tool to query Roughtime servers.
Install roughly with the cli extra using your favorite CLI package manager, for example with uv (or pipx):
# Assuming you cloned the repository
uv tool install .[cli]
pipx install .[cli]Then you can query a Roughtime server like so:
roughly query time.teax.dev 2002 84pMADvKUcSOq5RNbVRjVrjiU16Dxo2XV2Qkm+4DRTg=Or run ecosystem queries (assuming you have an ecosystem.json file):
roughly ecosystem malfeasance
roughly ecosystem stateYou can also run your own Roughtime server using roughly.
First, generate a keypair:
roughly server keygenThis will output a .env file containing the server's private key.
You can then run the server like so:
ROUGHLY_SERVER_PRIVATE_KEY="your_private_key_here" roughly -v server runBy default, the server will bind to 0.0.0.0:2002. You can change this using the --host and --port flags.
I recommend running the server with verbose logging enabled (-v), so you can see incoming requests and debug any issues.
roughly can be used as an asynchronous library to query Roughtime servers from your own Python code.
import roughly.client
response = await roughly.client.send_request(
host="time.teax.dev",
port=2002,
public_key=base64.b64decode(b"84pMADvKUcSOq5RNbVRjVrjiU16Dxo2XV2Qkm+4DRTg=")
)
# Responses are always verified before being returned
print("Current time:", response.signed_response.midpoint)You can also use the built-in ecosystem tools to query multiple servers and check for malfeasance as described in the RFC.
from pathlib import Path
import json
from roughly.ecosystem import (
confirm_malfeasance,
load_ecosystem,
malfeasance_report,
pick_servers,
query_servers,
)
ecosystem = load_ecosystem(Path("ecosystem.json"))
selected_servers = await pick_servers(ecosystem)
responses = await query_servers(selected_servers)
report = malfeasance_report(responses, selected_servers)
if confirm_malfeasance(report):
print("something scary is going on!")
with open("malfeasance_report.json", "w") as f:
json.dump(report, f, indent=2)You can also programmatically run your own Roughtime server:
import roughly.server
server = roughly.server.Server.create() # generates a new keypair
await roughly.server.serve(server)Why? You can subclass roughly.server.UDPHandler and roughly.server.Server to implement custom behavior. Like a malfeasant server for testing:
import roughly
import roughly.server
class ScaryServer(roughly.server.Server):
@staticmethod
def get_time() -> int:
# return a wrong-ish time
return int(time.time()) + random.randint(-3600, 3600)
await roughly.server.serve(ScaryServer.create())An example ecosystem file can be found at ecosystem.json, I tried my best to include as many servers as I could find.
If you know of any other Roughtime servers, run your own server, or have updated public keys for any of the listed servers, please open a PR or an issue!
The interopability matrix of roughly against Roughtime servers looks like this:
| Server | Result |
|---|---|
| butterfield | ✅ |
| cloudflare | ✅ |
| pyroughtime | ✅ |
| roughenough | |
| roughtimed | ✅ |
| roughly | ✅ |
roughenough only expects version 0x8000000c and does not ignore unknown versions.
Make sure to explicitly request only version 0x8000000c when querying roughenough servers, i.e.:
await roughly.client.send_request(
# <snip!>
versions=(0x8000000c,),
)| Client | Result |
|---|---|
| cloudflare | ✅ |
| craggy | ✅ |
| node-roughtime | ✅ |
| pyroughtime | ✅ |
| roughenough | ❌ |
| roughly | ✅ |
| vroughtime | ✅ |
Support for draft-7 is limited, in the sense that roughly will fit responses from draft-7 servers into the draft-15 data structures.
This means that some fields that are not present in draft-8+ (such as DUT1, DTAI, and LEAP) will be missing.
Additionally draft-7 offered for the precision of radius to be in microseconds, while draft-8+ uses seconds, this precision will be lost when querying draft-7 servers, and be clamped to a minimum of one second.
Throughout the codebase, comments beginning with # VDIFF mark sections that accommodate differences between Roughtime protocol drafts. These annotations help track changes made for compatibility and make it easier to identify code adjusted for specific draft versions.
This project is licensed under the MIT License. See the LICENSE file for details.