Skip to content

Repository files navigation

roughly

RuffGitHub Actions Workflow StatusCoverallsRoughtime draft 07-19

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

Every server run option can also be set through an environment variable, named after the flag with a ROUGHLY_ prefix. Check roughly server run --help for more information.

As a library

Querying

roughly can be used as an asynchronous library to query Roughtime servers from your own Python code.

importroughly.client# <snip!>response=awaitroughly.client.send_request(
host="time.teax.dev",
port=2002,
public_key=base64.b64decode(b"84pMADvKUcSOq5RNbVRjVrjiU16Dxo2XV2Qkm+4DRTg="),
)
midpoint=response.signed_response.midpointradius=response.signed_response.radiusprint(f"time: {midpoint} ± {radius}s")

send_request verifies the response before returning. Any failure raises roughly.errors.VerificationError; malformed packets raise PacketError. Both inherit from RoughtimeError. Once you have a response, true time is somewhere in the range [midpoint - radius, midpoint + radius].

An ecosystem is a list of servers a client can query. roughly.ecosystem provides a flow for querying them and checking for disagreement:

frompathlibimportPathimportjsonfromroughly.ecosystemimport (
confirm_malfeasance,
load_ecosystem,
malfeasance_report,
pick_servers,
query_servers,
)
ecosystem=load_ecosystem(Path("ecosystem.json"))
selected_servers=awaitpick_servers(ecosystem)
responses=awaitquery_servers(selected_servers)
report=malfeasance_report(responses, selected_servers)
ifconfirm_malfeasance(report):
print("something scary is going on!")
withopen("malfeasance_report.json", "w") asf:
json.dump(report, f, indent=2)

pick_servers filters down to servers that are actually reachable right now. query_servers returns one (VerifiableResponse, raw_bytes) per server. confirm_malfeasance returns true when the responses can't all be true at the same time.

Running a server

You can also programmatically run your own Roughtime server. Server.create() mints a long-term ed25519 keypair on each call, so for any server clients should be able to keep talking to across restarts, pass private_key=... with a key you've persisted yourself:

importroughly.serverserver=roughly.server.Server.create() # generates a fresh keypairawaitroughly.server.serve(server)

The reason to use the library directly instead of the CLI is that both roughly.server.Server and roughly.server.UDPHandler are designed to be extended. A sample use case is a deliberately malfeasant server:

importroughlyimportroughly.serverclassScaryServer(roughly.server.Server):
@staticmethoddefget_time() ->int:
# return a wrong-ish timereturnint(time.time()) +random.randint(-3600, 3600)
awaitroughly.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

ServerResult
butterfield
cloudflare
pyroughtime
roughenough
roughtimed
roughly
tannerryan-roughtime

Roughly as a server

ClientResult
cloudflare
craggy
node-roughtime
pyroughtime
roughenough
roughly
tannerryan-roughtime
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.

About

An asynchronous Python implementation of the Roughtime protocol

Topics

Resources

Stars

5 stars

Watchers

1 watching

Forks

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
GitHub - teaishealthy/roughly: An asynchronous Python implementation of the Roughtime protocol · GitHub
Skip to content

Repository files navigation

roughly

RuffGitHub Actions Workflow StatusCoverallsRoughtime draft 07-19

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

Every server run option can also be set through an environment variable, named after the flag with a ROUGHLY_ prefix. Check roughly server run --help for more information.

As a library

Querying

roughly can be used as an asynchronous library to query Roughtime servers from your own Python code.

importroughly.client# <snip!>response=awaitroughly.client.send_request(
host="time.teax.dev",
port=2002,
public_key=base64.b64decode(b"84pMADvKUcSOq5RNbVRjVrjiU16Dxo2XV2Qkm+4DRTg="),
)
midpoint=response.signed_response.midpointradius=response.signed_response.radiusprint(f"time: {midpoint} ± {radius}s")

send_request verifies the response before returning. Any failure raises roughly.errors.VerificationError; malformed packets raise PacketError. Both inherit from RoughtimeError. Once you have a response, true time is somewhere in the range [midpoint - radius, midpoint + radius].

An ecosystem is a list of servers a client can query. roughly.ecosystem provides a flow for querying them and checking for disagreement:

frompathlibimportPathimportjsonfromroughly.ecosystemimport (
confirm_malfeasance,
load_ecosystem,
malfeasance_report,
pick_servers,
query_servers,
)
ecosystem=load_ecosystem(Path("ecosystem.json"))
selected_servers=awaitpick_servers(ecosystem)
responses=awaitquery_servers(selected_servers)
report=malfeasance_report(responses, selected_servers)
ifconfirm_malfeasance(report):
print("something scary is going on!")
withopen("malfeasance_report.json", "w") asf:
json.dump(report, f, indent=2)

pick_servers filters down to servers that are actually reachable right now. query_servers returns one (VerifiableResponse, raw_bytes) per server. confirm_malfeasance returns true when the responses can't all be true at the same time.

Running a server

You can also programmatically run your own Roughtime server. Server.create() mints a long-term ed25519 keypair on each call, so for any server clients should be able to keep talking to across restarts, pass private_key=... with a key you've persisted yourself:

importroughly.serverserver=roughly.server.Server.create() # generates a fresh keypairawaitroughly.server.serve(server)

The reason to use the library directly instead of the CLI is that both roughly.server.Server and roughly.server.UDPHandler are designed to be extended. A sample use case is a deliberately malfeasant server:

importroughlyimportroughly.serverclassScaryServer(roughly.server.Server):
@staticmethoddefget_time() ->int:
# return a wrong-ish timereturnint(time.time()) +random.randint(-3600, 3600)
awaitroughly.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

ServerResult
butterfield
cloudflare
pyroughtime
roughenough
roughtimed
roughly
tannerryan-roughtime

Roughly as a server

ClientResult
cloudflare
craggy
node-roughtime
pyroughtime
roughenough
roughly
tannerryan-roughtime
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.

About

An asynchronous Python implementation of the Roughtime protocol

Topics

Resources

Stars

5 stars

Watchers

1 watching

Forks

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - teaishealthy/roughly: An asynchronous Python implementation of the Roughtime protocol · GitHub
Skip to content

Repository files navigation

roughly

RuffGitHub Actions Workflow StatusCoverallsRoughtime draft 07-19

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

Every server run option can also be set through an environment variable, named after the flag with a ROUGHLY_ prefix. Check roughly server run --help for more information.

As a library

Querying

roughly can be used as an asynchronous library to query Roughtime servers from your own Python code.

importroughly.client# <snip!>response=awaitroughly.client.send_request(
host="time.teax.dev",
port=2002,
public_key=base64.b64decode(b"84pMADvKUcSOq5RNbVRjVrjiU16Dxo2XV2Qkm+4DRTg="),
)
midpoint=response.signed_response.midpointradius=response.signed_response.radiusprint(f"time: {midpoint} ± {radius}s")

send_request verifies the response before returning. Any failure raises roughly.errors.VerificationError; malformed packets raise PacketError. Both inherit from RoughtimeError. Once you have a response, true time is somewhere in the range [midpoint - radius, midpoint + radius].

An ecosystem is a list of servers a client can query. roughly.ecosystem provides a flow for querying them and checking for disagreement:

frompathlibimportPathimportjsonfromroughly.ecosystemimport (
confirm_malfeasance,
load_ecosystem,
malfeasance_report,
pick_servers,
query_servers,
)
ecosystem=load_ecosystem(Path("ecosystem.json"))
selected_servers=awaitpick_servers(ecosystem)
responses=awaitquery_servers(selected_servers)
report=malfeasance_report(responses, selected_servers)
ifconfirm_malfeasance(report):
print("something scary is going on!")
withopen("malfeasance_report.json", "w") asf:
json.dump(report, f, indent=2)

pick_servers filters down to servers that are actually reachable right now. query_servers returns one (VerifiableResponse, raw_bytes) per server. confirm_malfeasance returns true when the responses can't all be true at the same time.

Running a server

You can also programmatically run your own Roughtime server. Server.create() mints a long-term ed25519 keypair on each call, so for any server clients should be able to keep talking to across restarts, pass private_key=... with a key you've persisted yourself:

importroughly.serverserver=roughly.server.Server.create() # generates a fresh keypairawaitroughly.server.serve(server)

The reason to use the library directly instead of the CLI is that both roughly.server.Server and roughly.server.UDPHandler are designed to be extended. A sample use case is a deliberately malfeasant server:

importroughlyimportroughly.serverclassScaryServer(roughly.server.Server):
@staticmethoddefget_time() ->int:
# return a wrong-ish timereturnint(time.time()) +random.randint(-3600, 3600)
awaitroughly.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

ServerResult
butterfield
cloudflare
pyroughtime
roughenough
roughtimed
roughly
tannerryan-roughtime

Roughly as a server

ClientResult
cloudflare
craggy
node-roughtime
pyroughtime
roughenough
roughly
tannerryan-roughtime
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.

About

An asynchronous Python implementation of the Roughtime protocol

Topics

Resources

Stars

5 stars

Watchers

1 watching

Forks

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - teaishealthy/roughly: An asynchronous Python implementation of the Roughtime protocol · GitHub
Skip to content

Repository files navigation

roughly

RuffGitHub Actions Workflow StatusCoverallsRoughtime draft 07-19

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

Every server run option can also be set through an environment variable, named after the flag with a ROUGHLY_ prefix. Check roughly server run --help for more information.

As a library

Querying

roughly can be used as an asynchronous library to query Roughtime servers from your own Python code.

importroughly.client# <snip!>response=awaitroughly.client.send_request(
host="time.teax.dev",
port=2002,
public_key=base64.b64decode(b"84pMADvKUcSOq5RNbVRjVrjiU16Dxo2XV2Qkm+4DRTg="),
)
midpoint=response.signed_response.midpointradius=response.signed_response.radiusprint(f"time: {midpoint} ± {radius}s")

send_request verifies the response before returning. Any failure raises roughly.errors.VerificationError; malformed packets raise PacketError. Both inherit from RoughtimeError. Once you have a response, true time is somewhere in the range [midpoint - radius, midpoint + radius].

An ecosystem is a list of servers a client can query. roughly.ecosystem provides a flow for querying them and checking for disagreement:

frompathlibimportPathimportjsonfromroughly.ecosystemimport (
confirm_malfeasance,
load_ecosystem,
malfeasance_report,
pick_servers,
query_servers,
)
ecosystem=load_ecosystem(Path("ecosystem.json"))
selected_servers=awaitpick_servers(ecosystem)
responses=awaitquery_servers(selected_servers)
report=malfeasance_report(responses, selected_servers)
ifconfirm_malfeasance(report):
print("something scary is going on!")
withopen("malfeasance_report.json", "w") asf:
json.dump(report, f, indent=2)

pick_servers filters down to servers that are actually reachable right now. query_servers returns one (VerifiableResponse, raw_bytes) per server. confirm_malfeasance returns true when the responses can't all be true at the same time.

Running a server

You can also programmatically run your own Roughtime server. Server.create() mints a long-term ed25519 keypair on each call, so for any server clients should be able to keep talking to across restarts, pass private_key=... with a key you've persisted yourself:

importroughly.serverserver=roughly.server.Server.create() # generates a fresh keypairawaitroughly.server.serve(server)

The reason to use the library directly instead of the CLI is that both roughly.server.Server and roughly.server.UDPHandler are designed to be extended. A sample use case is a deliberately malfeasant server:

importroughlyimportroughly.serverclassScaryServer(roughly.server.Server):
@staticmethoddefget_time() ->int:
# return a wrong-ish timereturnint(time.time()) +random.randint(-3600, 3600)
awaitroughly.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

ServerResult
butterfield
cloudflare
pyroughtime
roughenough
roughtimed
roughly
tannerryan-roughtime

Roughly as a server

ClientResult
cloudflare
craggy
node-roughtime
pyroughtime
roughenough
roughly
tannerryan-roughtime
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.

About

An asynchronous Python implementation of the Roughtime protocol

Topics

Resources

Stars

5 stars

Watchers

1 watching

Forks

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' GitHub - teaishealthy/roughly: An asynchronous Python implementation of the Roughtime protocol · GitHub
Skip to content

Repository files navigation

roughly

RuffGitHub Actions Workflow StatusCoverallsRoughtime draft 07-19

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

Every server run option can also be set through an environment variable, named after the flag with a ROUGHLY_ prefix. Check roughly server run --help for more information.

As a library

Querying

roughly can be used as an asynchronous library to query Roughtime servers from your own Python code.

importroughly.client# <snip!>response=awaitroughly.client.send_request(
host="time.teax.dev",
port=2002,
public_key=base64.b64decode(b"84pMADvKUcSOq5RNbVRjVrjiU16Dxo2XV2Qkm+4DRTg="),
)
midpoint=response.signed_response.midpointradius=response.signed_response.radiusprint(f"time: {midpoint} ± {radius}s")

send_request verifies the response before returning. Any failure raises roughly.errors.VerificationError; malformed packets raise PacketError. Both inherit from RoughtimeError. Once you have a response, true time is somewhere in the range [midpoint - radius, midpoint + radius].

An ecosystem is a list of servers a client can query. roughly.ecosystem provides a flow for querying them and checking for disagreement:

frompathlibimportPathimportjsonfromroughly.ecosystemimport (
confirm_malfeasance,
load_ecosystem,
malfeasance_report,
pick_servers,
query_servers,
)
ecosystem=load_ecosystem(Path("ecosystem.json"))
selected_servers=awaitpick_servers(ecosystem)
responses=awaitquery_servers(selected_servers)
report=malfeasance_report(responses, selected_servers)
ifconfirm_malfeasance(report):
print("something scary is going on!")
withopen("malfeasance_report.json", "w") asf:
json.dump(report, f, indent=2)

pick_servers filters down to servers that are actually reachable right now. query_servers returns one (VerifiableResponse, raw_bytes) per server. confirm_malfeasance returns true when the responses can't all be true at the same time.

Running a server

You can also programmatically run your own Roughtime server. Server.create() mints a long-term ed25519 keypair on each call, so for any server clients should be able to keep talking to across restarts, pass private_key=... with a key you've persisted yourself:

importroughly.serverserver=roughly.server.Server.create() # generates a fresh keypairawaitroughly.server.serve(server)

The reason to use the library directly instead of the CLI is that both roughly.server.Server and roughly.server.UDPHandler are designed to be extended. A sample use case is a deliberately malfeasant server:

importroughlyimportroughly.serverclassScaryServer(roughly.server.Server):
@staticmethoddefget_time() ->int:
# return a wrong-ish timereturnint(time.time()) +random.randint(-3600, 3600)
awaitroughly.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

ServerResult
butterfield
cloudflare
pyroughtime
roughenough
roughtimed
roughly
tannerryan-roughtime

Roughly as a server

ClientResult
cloudflare
craggy
node-roughtime
pyroughtime
roughenough
roughly
tannerryan-roughtime
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.

About

An asynchronous Python implementation of the Roughtime protocol

Topics

Resources

Stars

5 stars

Watchers

1 watching

Forks

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - teaishealthy/roughly: An asynchronous Python implementation of the Roughtime protocol · GitHub
Skip to content

Repository files navigation

roughly

RuffGitHub Actions Workflow StatusCoverallsRoughtime draft 07-19

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

Every server run option can also be set through an environment variable, named after the flag with a ROUGHLY_ prefix. Check roughly server run --help for more information.

As a library

Querying

roughly can be used as an asynchronous library to query Roughtime servers from your own Python code.

importroughly.client# <snip!>response=awaitroughly.client.send_request(
host="time.teax.dev",
port=2002,
public_key=base64.b64decode(b"84pMADvKUcSOq5RNbVRjVrjiU16Dxo2XV2Qkm+4DRTg="),
)
midpoint=response.signed_response.midpointradius=response.signed_response.radiusprint(f"time: {midpoint} ± {radius}s")

send_request verifies the response before returning. Any failure raises roughly.errors.VerificationError; malformed packets raise PacketError. Both inherit from RoughtimeError. Once you have a response, true time is somewhere in the range [midpoint - radius, midpoint + radius].

An ecosystem is a list of servers a client can query. roughly.ecosystem provides a flow for querying them and checking for disagreement:

frompathlibimportPathimportjsonfromroughly.ecosystemimport (
confirm_malfeasance,
load_ecosystem,
malfeasance_report,
pick_servers,
query_servers,
)
ecosystem=load_ecosystem(Path("ecosystem.json"))
selected_servers=awaitpick_servers(ecosystem)
responses=awaitquery_servers(selected_servers)
report=malfeasance_report(responses, selected_servers)
ifconfirm_malfeasance(report):
print("something scary is going on!")
withopen("malfeasance_report.json", "w") asf:
json.dump(report, f, indent=2)

pick_servers filters down to servers that are actually reachable right now. query_servers returns one (VerifiableResponse, raw_bytes) per server. confirm_malfeasance returns true when the responses can't all be true at the same time.

Running a server

You can also programmatically run your own Roughtime server. Server.create() mints a long-term ed25519 keypair on each call, so for any server clients should be able to keep talking to across restarts, pass private_key=... with a key you've persisted yourself:

importroughly.serverserver=roughly.server.Server.create() # generates a fresh keypairawaitroughly.server.serve(server)

The reason to use the library directly instead of the CLI is that both roughly.server.Server and roughly.server.UDPHandler are designed to be extended. A sample use case is a deliberately malfeasant server:

importroughlyimportroughly.serverclassScaryServer(roughly.server.Server):
@staticmethoddefget_time() ->int:
# return a wrong-ish timereturnint(time.time()) +random.randint(-3600, 3600)
awaitroughly.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

ServerResult
butterfield
cloudflare
pyroughtime
roughenough
roughtimed
roughly
tannerryan-roughtime

Roughly as a server

ClientResult
cloudflare
craggy
node-roughtime
pyroughtime
roughenough
roughly
tannerryan-roughtime
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.

About

An asynchronous Python implementation of the Roughtime protocol

Topics

Resources

Stars

5 stars

Watchers

1 watching

Forks

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - teaishealthy/roughly: An asynchronous Python implementation of the Roughtime protocol · GitHub
Skip to content

Repository files navigation

roughly

RuffGitHub Actions Workflow StatusCoverallsRoughtime draft 07-19

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

Every server run option can also be set through an environment variable, named after the flag with a ROUGHLY_ prefix. Check roughly server run --help for more information.

As a library

Querying

roughly can be used as an asynchronous library to query Roughtime servers from your own Python code.

importroughly.client# <snip!>response=awaitroughly.client.send_request(
host="time.teax.dev",
port=2002,
public_key=base64.b64decode(b"84pMADvKUcSOq5RNbVRjVrjiU16Dxo2XV2Qkm+4DRTg="),
)
midpoint=response.signed_response.midpointradius=response.signed_response.radiusprint(f"time: {midpoint} ± {radius}s")

send_request verifies the response before returning. Any failure raises roughly.errors.VerificationError; malformed packets raise PacketError. Both inherit from RoughtimeError. Once you have a response, true time is somewhere in the range [midpoint - radius, midpoint + radius].

An ecosystem is a list of servers a client can query. roughly.ecosystem provides a flow for querying them and checking for disagreement:

frompathlibimportPathimportjsonfromroughly.ecosystemimport (
confirm_malfeasance,
load_ecosystem,
malfeasance_report,
pick_servers,
query_servers,
)
ecosystem=load_ecosystem(Path("ecosystem.json"))
selected_servers=awaitpick_servers(ecosystem)
responses=awaitquery_servers(selected_servers)
report=malfeasance_report(responses, selected_servers)
ifconfirm_malfeasance(report):
print("something scary is going on!")
withopen("malfeasance_report.json", "w") asf:
json.dump(report, f, indent=2)

pick_servers filters down to servers that are actually reachable right now. query_servers returns one (VerifiableResponse, raw_bytes) per server. confirm_malfeasance returns true when the responses can't all be true at the same time.

Running a server

You can also programmatically run your own Roughtime server. Server.create() mints a long-term ed25519 keypair on each call, so for any server clients should be able to keep talking to across restarts, pass private_key=... with a key you've persisted yourself:

importroughly.serverserver=roughly.server.Server.create() # generates a fresh keypairawaitroughly.server.serve(server)

The reason to use the library directly instead of the CLI is that both roughly.server.Server and roughly.server.UDPHandler are designed to be extended. A sample use case is a deliberately malfeasant server:

importroughlyimportroughly.serverclassScaryServer(roughly.server.Server):
@staticmethoddefget_time() ->int:
# return a wrong-ish timereturnint(time.time()) +random.randint(-3600, 3600)
awaitroughly.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

ServerResult
butterfield
cloudflare
pyroughtime
roughenough
roughtimed
roughly
tannerryan-roughtime

Roughly as a server

ClientResult
cloudflare
craggy
node-roughtime
pyroughtime
roughenough
roughly
tannerryan-roughtime
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.

About

An asynchronous Python implementation of the Roughtime protocol

Topics

Resources

Stars

5 stars

Watchers

1 watching

Forks

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); GitHub - teaishealthy/roughly: An asynchronous Python implementation of the Roughtime protocol · GitHub
Skip to content

Repository files navigation

roughly

RuffGitHub Actions Workflow StatusCoverallsRoughtime draft 07-19

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

Every server run option can also be set through an environment variable, named after the flag with a ROUGHLY_ prefix. Check roughly server run --help for more information.

As a library

Querying

roughly can be used as an asynchronous library to query Roughtime servers from your own Python code.

importroughly.client# <snip!>response=awaitroughly.client.send_request(
host="time.teax.dev",
port=2002,
public_key=base64.b64decode(b"84pMADvKUcSOq5RNbVRjVrjiU16Dxo2XV2Qkm+4DRTg="),
)
midpoint=response.signed_response.midpointradius=response.signed_response.radiusprint(f"time: {midpoint} ± {radius}s")

send_request verifies the response before returning. Any failure raises roughly.errors.VerificationError; malformed packets raise PacketError. Both inherit from RoughtimeError. Once you have a response, true time is somewhere in the range [midpoint - radius, midpoint + radius].

An ecosystem is a list of servers a client can query. roughly.ecosystem provides a flow for querying them and checking for disagreement:

frompathlibimportPathimportjsonfromroughly.ecosystemimport (
confirm_malfeasance,
load_ecosystem,
malfeasance_report,
pick_servers,
query_servers,
)
ecosystem=load_ecosystem(Path("ecosystem.json"))
selected_servers=awaitpick_servers(ecosystem)
responses=awaitquery_servers(selected_servers)
report=malfeasance_report(responses, selected_servers)
ifconfirm_malfeasance(report):
print("something scary is going on!")
withopen("malfeasance_report.json", "w") asf:
json.dump(report, f, indent=2)

pick_servers filters down to servers that are actually reachable right now. query_servers returns one (VerifiableResponse, raw_bytes) per server. confirm_malfeasance returns true when the responses can't all be true at the same time.

Running a server

You can also programmatically run your own Roughtime server. Server.create() mints a long-term ed25519 keypair on each call, so for any server clients should be able to keep talking to across restarts, pass private_key=... with a key you've persisted yourself:

importroughly.serverserver=roughly.server.Server.create() # generates a fresh keypairawaitroughly.server.serve(server)

The reason to use the library directly instead of the CLI is that both roughly.server.Server and roughly.server.UDPHandler are designed to be extended. A sample use case is a deliberately malfeasant server:

importroughlyimportroughly.serverclassScaryServer(roughly.server.Server):
@staticmethoddefget_time() ->int:
# return a wrong-ish timereturnint(time.time()) +random.randint(-3600, 3600)
awaitroughly.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

ServerResult
butterfield
cloudflare
pyroughtime
roughenough
roughtimed
roughly
tannerryan-roughtime

Roughly as a server

ClientResult
cloudflare
craggy
node-roughtime
pyroughtime
roughenough
roughly
tannerryan-roughtime
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.

About

An asynchronous Python implementation of the Roughtime protocol

Topics

Resources

Stars

5 stars

Watchers

1 watching

Forks

Used by

Contributors

Languages