Skip to main content

An asynchronous Roughtime implementation for Python

Project description

roughly

Ruff GitHub Actions Workflow Status Coveralls Roughtime draft 07-19 WIP

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-19.

Draft versions 07 through 19 are supported for querying servers.
Draft versions 10 through 19 are supported for running a server. Also supports queries from Google Roughtime clients.

Quickstart

Installation

You can install roughly from PyPI using your favorite package manager, for example with pip:

pip install roughly
# or with the cli extra
pip install roughly[cli]

As a CLI

Querying

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):

uv tool install roughly[cli]
pipx install roughly[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 state

Running a server

You can also run your own Roughtime server using roughly.

First, generate a keypair:

roughly server keygen

This 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 run

By 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. Additionally you might want to consider turning off response greasing while testing using the --no-grease flag.

As a library

Querying

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)

Running a server

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())

Ecosystem

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!

Interoperability

The interopability matrix of roughly against Roughtime servers looks like this:

Roughly as a client

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,),
)

Roughly as a server

Client Result
cloudflare
craggy
node-roughtime
pyroughtime
roughenough
roughly
vroughtime

draft-7

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.

License

This project is licensed under the MIT License. See the LICENSE file for details.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

roughly-0.1.0.tar.gz (22.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

roughly-0.1.0-py3-none-any.whl (27.4 kB view details)

Uploaded Python 3

File details

Details for the file roughly-0.1.0.tar.gz.

File metadata

  • Download URL: roughly-0.1.0.tar.gz
  • Upload date:
  • Size: 22.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.16 {"installer":{"name":"uv","version":"0.11.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Arch Linux","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for roughly-0.1.0.tar.gz
Algorithm Hash digest
SHA256 62f942bba613d4658456cdc68ac1a05c1c574bc94be76b7135a5fdc15402c869
MD5 38118791596c58b38419e9dc6454e392
BLAKE2b-256 54450933c841a9ebbd125ed6cc4ae9847c86c680d6731c3def5c8eafad187a54

See more details on using hashes here.

File details

Details for the file roughly-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: roughly-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 27.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.16 {"installer":{"name":"uv","version":"0.11.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Arch Linux","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for roughly-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e81492740abc82ec02b72b9bd190d4837935b8cee565ddd6ca2b296c775e2769
MD5 ada3799f8208f992cfc0782d62a20cd6
BLAKE2b-256 f6260d6e49523245e573abc43ea4a90f932d6b38b24411be7c230567d9566396

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page