Repository files navigation

roughtime

Go ReferenceLicense

A Go implementation of Google-Roughtime and IETF Roughtime drafts 01–19. The repository contains a high-level client package, the low-level protocol package, a high-throughput server, and client, debug, benchmark, and document-stamping commands.

Drafts 12–19 share wire version 0x8000000c. Drafts 14–19 add the TYPE exchange without changing that value, so clients must support both typed and untyped peers.

Try the public server:

go run ./cmd/roughtime-client -addr time.txryan.com:2002 \
-pubkey iBVjxg/1j7y1+kQUTBYdTabxCppesU/07D4PMDJk2WA=

ML-DSA-44 support is an experimental, non-IETF extension. It uses the FIPS 204 context parameter and TCP framing because its replies exceed the UDP amplification budget. It is not interoperable with standard Roughtime implementations.

Build

Go 1.27 or newer is required.

make build

This produces roughtime, roughtime-client, roughtime-debug, roughtime-bench, and roughtime-stamp. Run any command with -h for its complete flag list.

Server

The server accepts IETF Ed25519 requests over UDP and TCP and Google-Roughtime over UDP. The experimental ML-DSA-44 suite uses TCP only. The Linux UDP path uses one SO_REUSEPORT socket per GOMAXPROCS worker and batched I/O. OpenBSD 7.2 or later is required, for its recvmmsg/sendmmsg. Batching on OpenBSD amortizes syscalls but still signs on one goroutine per socket, and because OpenBSD has no IPv4-mapped IPv6 a wildcard bind gets one socket per address family. Other Unix systems use a portable socket loop. Windows is not supported.

Generate a root key and start the server:

roughtime -keygen /path/to/root.key
roughtime -root-key-file /path/to/root.key

Use -pq-keygen and -pq-root-key-file for ML-DSA-44, or configure both root files. Seed-file permissions are no broader than 0600; existing files are never overwritten. Online delegation certificates refresh automatically. With -offline-delegation, root files are read only during startup and the server stops when the delegation leaves its validity window.

By default the server greases 1% of responses to exercise client error paths; set -grease-rate 0 when deterministic replies are required.

UDP and TCP share -port (default 2002). On multihomed hosts, -listen-address binds both listeners to the advertised local address so UDP replies retain that source. -metrics-addr enables unauthenticated Prometheus /metrics and /healthz endpoints; bind it to loopback unless an external access-control layer protects it.

Docker

docker build -t roughtime .
mkdir -p keys
docker run --rm --user "$(id -u):$(id -g)" -v "$PWD/keys:/keys" \
roughtime -keygen /keys/root.key
docker run --read-only --user "$(id -u):$(id -g)" \
--cap-drop ALL --security-opt no-new-privileges \
-p 2002:2002/udp -p 2002:2002/tcp -v "$PWD/keys:/keys:ro" \
roughtime -root-key-file /keys/root.key

The example uses the host user so the generated private key remains readable. Without --user, the runtime image uses UID 65532 and mounted files must be readable by that UID.

Commands

roughtime-client

Query one server with -addr and -pubkey, or an ecosystem with -servers. Multi-server ecosystem queries form a causal chain unless -chain=false is set. The default samples up to five endpoint-domain groups; -all disables sampling and queries every transport-compatible entry. That grouping is a diversity heuristic, not authenticated operator identity or Sybil resistance.

go run ./cmd/roughtime-client -servers ecosystem.json -all

roughtime-debug

Probe supported versions and inspect authenticated response structure and timing data. Draft 12 is tried in typed and untyped forms.

go run ./cmd/roughtime-debug -addr time.txryan.com:2002 \
-pubkey iBVjxg/1j7y1+kQUTBYdTabxCppesU/07D4PMDJk2WA=

roughtime-bench

A closed-loop load generator intended for servers you control. -verify checks each signature and Merkle proof; without it, results measure transport only and may count malformed replies.

go run ./cmd/roughtime-bench -addr 127.0.0.1:2002 -pubkey <key> \
-workers 64 -duration 10s -verify

roughtime-stamp

Create or verify a document timestamp receipt. New receipts select three compatible endpoint-domain groups and query them twice in the same order. The document is hashed before querying and again immediately before the proof is durably persisted. Verification checks the document, the full causal chain, and the trusted ecosystem. Existing one-pass proofs remain readable and are reported as legacy receipts.

go run ./cmd/roughtime-stamp -doc README.md -servers ecosystem.json \
-out README.md.proof
go run ./cmd/roughtime-stamp -mode verify -doc README.md \
-servers ecosystem.json -in README.md.proof

Go API

The top-level package provides Client.Query, concurrent QueryAll, causal QueryChain, document-bound QueryChainWithNonce, consensus helpers, proof serialization and offline verification, and ecosystem parsing.

pk, err:=roughtime.DecodePublicKey(encodedKey)
iferr!=nil {
returnerr
}
server:= roughtime.Server{
Name: "example",
PublicKey: pk,
Addresses: []roughtime.Address{{
Transport: "udp",
Address: "example.com:2002",
}},
}
response, err:=new(roughtime.Client).Query(ctx, server)

The protocol package exposes request parsing/building, reply creation and verification, transports, version negotiation, chaining, and malfeasance reports.

Compatibility details for low-level callers:

  • IETF request builders use a 1024-byte body; ML-DSA-44 uses 8192 bytes. The 12-byte ROUGHTIM frame is additional.
  • RequestOptions.LegacyPacketSize produces the historical 1024/8192-byte total packet size required by some deployed peers; the high-level client enables it for interoperability.
  • RequestOptions.OmitTYPE produces the untyped drafts 12/13 request.
  • VerifyOptions.RequireTYPE requires the draft-14+ typed exchange. The default verifier accepts both forms.
  • ReplyOptions.Draft14NodeFirst selects the node-first Merkle convention from drafts 14–15. The default builder remains hash-first for backward compatibility; verification accepts both node-first and draft-16+ hash-first proofs.
  • NewCertificateWithVersions binds the signed VERS list to a server's actual advertised versions.

ComputeSRV(rootPublicKey) returns the drafts 10+ server binding. Verifiers check negotiated versions, signed VERS, SRV, delegation validity, signatures, nonces, timestamps, and Merkle proofs.

Development

make deps # install development tools once
make test# tests
make test-race # race detector
make fuzz # retained parser/verifier fuzzers; FUZZ_TIME defaults to 30s
make lint # go vet and staticcheck
make vuln # reachable-vulnerability scan
make check # dependency, vendor, format, lint, security, build, race

The vendor tree is excluded from source edits and is checked for reproducibility with make verify-vendor.

License

Copyright (c) 2026 Tanner Ryan. All rights reserved. Use of this source code is governed by the BSD 2-Clause License.

About

Roughtime in Go: Google-Roughtime, IETF drafts 01-19, experimental post-quantum ML-DSA-44, and a production-ready server with automatic certificate refresh.

Topics

Resources

Contributing

Security policy

Stars

8 stars

Watchers

1 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n 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;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

roughtime

Go ReferenceLicense

A Go implementation of Google-Roughtime and IETF Roughtime drafts 01–19. The repository contains a high-level client package, the low-level protocol package, a high-throughput server, and client, debug, benchmark, and document-stamping commands.

Drafts 12–19 share wire version 0x8000000c. Drafts 14–19 add the TYPE exchange without changing that value, so clients must support both typed and untyped peers.

Try the public server:

go run ./cmd/roughtime-client -addr time.txryan.com:2002 \
-pubkey iBVjxg/1j7y1+kQUTBYdTabxCppesU/07D4PMDJk2WA=

ML-DSA-44 support is an experimental, non-IETF extension. It uses the FIPS 204 context parameter and TCP framing because its replies exceed the UDP amplification budget. It is not interoperable with standard Roughtime implementations.

Build

Go 1.27 or newer is required.

make build

This produces roughtime, roughtime-client, roughtime-debug, roughtime-bench, and roughtime-stamp. Run any command with -h for its complete flag list.

Server

The server accepts IETF Ed25519 requests over UDP and TCP and Google-Roughtime over UDP. The experimental ML-DSA-44 suite uses TCP only. The Linux UDP path uses one SO_REUSEPORT socket per GOMAXPROCS worker and batched I/O. OpenBSD 7.2 or later is required, for its recvmmsg/sendmmsg. Batching on OpenBSD amortizes syscalls but still signs on one goroutine per socket, and because OpenBSD has no IPv4-mapped IPv6 a wildcard bind gets one socket per address family. Other Unix systems use a portable socket loop. Windows is not supported.

Generate a root key and start the server:

roughtime -keygen /path/to/root.key
roughtime -root-key-file /path/to/root.key

Use -pq-keygen and -pq-root-key-file for ML-DSA-44, or configure both root files. Seed-file permissions are no broader than 0600; existing files are never overwritten. Online delegation certificates refresh automatically. With -offline-delegation, root files are read only during startup and the server stops when the delegation leaves its validity window.

By default the server greases 1% of responses to exercise client error paths; set -grease-rate 0 when deterministic replies are required.

UDP and TCP share -port (default 2002). On multihomed hosts, -listen-address binds both listeners to the advertised local address so UDP replies retain that source. -metrics-addr enables unauthenticated Prometheus /metrics and /healthz endpoints; bind it to loopback unless an external access-control layer protects it.

Docker

docker build -t roughtime .
mkdir -p keys
docker run --rm --user "$(id -u):$(id -g)" -v "$PWD/keys:/keys" \
roughtime -keygen /keys/root.key
docker run --read-only --user "$(id -u):$(id -g)" \
--cap-drop ALL --security-opt no-new-privileges \
-p 2002:2002/udp -p 2002:2002/tcp -v "$PWD/keys:/keys:ro" \
roughtime -root-key-file /keys/root.key

The example uses the host user so the generated private key remains readable. Without --user, the runtime image uses UID 65532 and mounted files must be readable by that UID.

Commands

roughtime-client

Query one server with -addr and -pubkey, or an ecosystem with -servers. Multi-server ecosystem queries form a causal chain unless -chain=false is set. The default samples up to five endpoint-domain groups; -all disables sampling and queries every transport-compatible entry. That grouping is a diversity heuristic, not authenticated operator identity or Sybil resistance.

go run ./cmd/roughtime-client -servers ecosystem.json -all

roughtime-debug

Probe supported versions and inspect authenticated response structure and timing data. Draft 12 is tried in typed and untyped forms.

go run ./cmd/roughtime-debug -addr time.txryan.com:2002 \
-pubkey iBVjxg/1j7y1+kQUTBYdTabxCppesU/07D4PMDJk2WA=

roughtime-bench

A closed-loop load generator intended for servers you control. -verify checks each signature and Merkle proof; without it, results measure transport only and may count malformed replies.

go run ./cmd/roughtime-bench -addr 127.0.0.1:2002 -pubkey <key> \
-workers 64 -duration 10s -verify

roughtime-stamp

Create or verify a document timestamp receipt. New receipts select three compatible endpoint-domain groups and query them twice in the same order. The document is hashed before querying and again immediately before the proof is durably persisted. Verification checks the document, the full causal chain, and the trusted ecosystem. Existing one-pass proofs remain readable and are reported as legacy receipts.

go run ./cmd/roughtime-stamp -doc README.md -servers ecosystem.json \
-out README.md.proof
go run ./cmd/roughtime-stamp -mode verify -doc README.md \
-servers ecosystem.json -in README.md.proof

Go API

The top-level package provides Client.Query, concurrent QueryAll, causal QueryChain, document-bound QueryChainWithNonce, consensus helpers, proof serialization and offline verification, and ecosystem parsing.

pk, err:=roughtime.DecodePublicKey(encodedKey)
iferr!=nil {
returnerr
}
server:= roughtime.Server{
Name: "example",
PublicKey: pk,
Addresses: []roughtime.Address{{
Transport: "udp",
Address: "example.com:2002",
}},
}
response, err:=new(roughtime.Client).Query(ctx, server)

The protocol package exposes request parsing/building, reply creation and verification, transports, version negotiation, chaining, and malfeasance reports.

Compatibility details for low-level callers:

  • IETF request builders use a 1024-byte body; ML-DSA-44 uses 8192 bytes. The 12-byte ROUGHTIM frame is additional.
  • RequestOptions.LegacyPacketSize produces the historical 1024/8192-byte total packet size required by some deployed peers; the high-level client enables it for interoperability.
  • RequestOptions.OmitTYPE produces the untyped drafts 12/13 request.
  • VerifyOptions.RequireTYPE requires the draft-14+ typed exchange. The default verifier accepts both forms.
  • ReplyOptions.Draft14NodeFirst selects the node-first Merkle convention from drafts 14–15. The default builder remains hash-first for backward compatibility; verification accepts both node-first and draft-16+ hash-first proofs.
  • NewCertificateWithVersions binds the signed VERS list to a server's actual advertised versions.

ComputeSRV(rootPublicKey) returns the drafts 10+ server binding. Verifiers check negotiated versions, signed VERS, SRV, delegation validity, signatures, nonces, timestamps, and Merkle proofs.

Development

make deps # install development tools once
make test# tests
make test-race # race detector
make fuzz # retained parser/verifier fuzzers; FUZZ_TIME defaults to 30s
make lint # go vet and staticcheck
make vuln # reachable-vulnerability scan
make check # dependency, vendor, format, lint, security, build, race

The vendor tree is excluded from source edits and is checked for reproducibility with make verify-vendor.

License

Copyright (c) 2026 Tanner Ryan. All rights reserved. Use of this source code is governed by the BSD 2-Clause License.

About

Roughtime in Go: Google-Roughtime, IETF drafts 01-19, experimental post-quantum ML-DSA-44, and a production-ready server with automatic certificate refresh.

Topics

Resources

Contributing

Security policy

Stars

8 stars

Watchers

1 watching

Forks

Releases

Used by

Contributors

Languages

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

Repository files navigation

roughtime

Go ReferenceLicense

A Go implementation of Google-Roughtime and IETF Roughtime drafts 01–19. The repository contains a high-level client package, the low-level protocol package, a high-throughput server, and client, debug, benchmark, and document-stamping commands.

Drafts 12–19 share wire version 0x8000000c. Drafts 14–19 add the TYPE exchange without changing that value, so clients must support both typed and untyped peers.

Try the public server:

go run ./cmd/roughtime-client -addr time.txryan.com:2002 \
-pubkey iBVjxg/1j7y1+kQUTBYdTabxCppesU/07D4PMDJk2WA=

ML-DSA-44 support is an experimental, non-IETF extension. It uses the FIPS 204 context parameter and TCP framing because its replies exceed the UDP amplification budget. It is not interoperable with standard Roughtime implementations.

Build

Go 1.27 or newer is required.

make build

This produces roughtime, roughtime-client, roughtime-debug, roughtime-bench, and roughtime-stamp. Run any command with -h for its complete flag list.

Server

The server accepts IETF Ed25519 requests over UDP and TCP and Google-Roughtime over UDP. The experimental ML-DSA-44 suite uses TCP only. The Linux UDP path uses one SO_REUSEPORT socket per GOMAXPROCS worker and batched I/O. OpenBSD 7.2 or later is required, for its recvmmsg/sendmmsg. Batching on OpenBSD amortizes syscalls but still signs on one goroutine per socket, and because OpenBSD has no IPv4-mapped IPv6 a wildcard bind gets one socket per address family. Other Unix systems use a portable socket loop. Windows is not supported.

Generate a root key and start the server:

roughtime -keygen /path/to/root.key
roughtime -root-key-file /path/to/root.key

Use -pq-keygen and -pq-root-key-file for ML-DSA-44, or configure both root files. Seed-file permissions are no broader than 0600; existing files are never overwritten. Online delegation certificates refresh automatically. With -offline-delegation, root files are read only during startup and the server stops when the delegation leaves its validity window.

By default the server greases 1% of responses to exercise client error paths; set -grease-rate 0 when deterministic replies are required.

UDP and TCP share -port (default 2002). On multihomed hosts, -listen-address binds both listeners to the advertised local address so UDP replies retain that source. -metrics-addr enables unauthenticated Prometheus /metrics and /healthz endpoints; bind it to loopback unless an external access-control layer protects it.

Docker

docker build -t roughtime .
mkdir -p keys
docker run --rm --user "$(id -u):$(id -g)" -v "$PWD/keys:/keys" \
roughtime -keygen /keys/root.key
docker run --read-only --user "$(id -u):$(id -g)" \
--cap-drop ALL --security-opt no-new-privileges \
-p 2002:2002/udp -p 2002:2002/tcp -v "$PWD/keys:/keys:ro" \
roughtime -root-key-file /keys/root.key

The example uses the host user so the generated private key remains readable. Without --user, the runtime image uses UID 65532 and mounted files must be readable by that UID.

Commands

roughtime-client

Query one server with -addr and -pubkey, or an ecosystem with -servers. Multi-server ecosystem queries form a causal chain unless -chain=false is set. The default samples up to five endpoint-domain groups; -all disables sampling and queries every transport-compatible entry. That grouping is a diversity heuristic, not authenticated operator identity or Sybil resistance.

go run ./cmd/roughtime-client -servers ecosystem.json -all

roughtime-debug

Probe supported versions and inspect authenticated response structure and timing data. Draft 12 is tried in typed and untyped forms.

go run ./cmd/roughtime-debug -addr time.txryan.com:2002 \
-pubkey iBVjxg/1j7y1+kQUTBYdTabxCppesU/07D4PMDJk2WA=

roughtime-bench

A closed-loop load generator intended for servers you control. -verify checks each signature and Merkle proof; without it, results measure transport only and may count malformed replies.

go run ./cmd/roughtime-bench -addr 127.0.0.1:2002 -pubkey <key> \
-workers 64 -duration 10s -verify

roughtime-stamp

Create or verify a document timestamp receipt. New receipts select three compatible endpoint-domain groups and query them twice in the same order. The document is hashed before querying and again immediately before the proof is durably persisted. Verification checks the document, the full causal chain, and the trusted ecosystem. Existing one-pass proofs remain readable and are reported as legacy receipts.

go run ./cmd/roughtime-stamp -doc README.md -servers ecosystem.json \
-out README.md.proof
go run ./cmd/roughtime-stamp -mode verify -doc README.md \
-servers ecosystem.json -in README.md.proof

Go API

The top-level package provides Client.Query, concurrent QueryAll, causal QueryChain, document-bound QueryChainWithNonce, consensus helpers, proof serialization and offline verification, and ecosystem parsing.

pk, err:=roughtime.DecodePublicKey(encodedKey)
iferr!=nil {
returnerr
}
server:= roughtime.Server{
Name: "example",
PublicKey: pk,
Addresses: []roughtime.Address{{
Transport: "udp",
Address: "example.com:2002",
}},
}
response, err:=new(roughtime.Client).Query(ctx, server)

The protocol package exposes request parsing/building, reply creation and verification, transports, version negotiation, chaining, and malfeasance reports.

Compatibility details for low-level callers:

  • IETF request builders use a 1024-byte body; ML-DSA-44 uses 8192 bytes. The 12-byte ROUGHTIM frame is additional.
  • RequestOptions.LegacyPacketSize produces the historical 1024/8192-byte total packet size required by some deployed peers; the high-level client enables it for interoperability.
  • RequestOptions.OmitTYPE produces the untyped drafts 12/13 request.
  • VerifyOptions.RequireTYPE requires the draft-14+ typed exchange. The default verifier accepts both forms.
  • ReplyOptions.Draft14NodeFirst selects the node-first Merkle convention from drafts 14–15. The default builder remains hash-first for backward compatibility; verification accepts both node-first and draft-16+ hash-first proofs.
  • NewCertificateWithVersions binds the signed VERS list to a server's actual advertised versions.

ComputeSRV(rootPublicKey) returns the drafts 10+ server binding. Verifiers check negotiated versions, signed VERS, SRV, delegation validity, signatures, nonces, timestamps, and Merkle proofs.

Development

make deps # install development tools once
make test# tests
make test-race # race detector
make fuzz # retained parser/verifier fuzzers; FUZZ_TIME defaults to 30s
make lint # go vet and staticcheck
make vuln # reachable-vulnerability scan
make check # dependency, vendor, format, lint, security, build, race

The vendor tree is excluded from source edits and is checked for reproducibility with make verify-vendor.

License

Copyright (c) 2026 Tanner Ryan. All rights reserved. Use of this source code is governed by the BSD 2-Clause License.

About

Roughtime in Go: Google-Roughtime, IETF drafts 01-19, experimental post-quantum ML-DSA-44, and a production-ready server with automatic certificate refresh.

Topics

Resources

Contributing

Security policy

Stars

8 stars

Watchers

1 watching

Forks

Releases

Used by

Contributors

Languages

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

Repository files navigation

roughtime

Go ReferenceLicense

A Go implementation of Google-Roughtime and IETF Roughtime drafts 01–19. The repository contains a high-level client package, the low-level protocol package, a high-throughput server, and client, debug, benchmark, and document-stamping commands.

Drafts 12–19 share wire version 0x8000000c. Drafts 14–19 add the TYPE exchange without changing that value, so clients must support both typed and untyped peers.

Try the public server:

go run ./cmd/roughtime-client -addr time.txryan.com:2002 \
-pubkey iBVjxg/1j7y1+kQUTBYdTabxCppesU/07D4PMDJk2WA=

ML-DSA-44 support is an experimental, non-IETF extension. It uses the FIPS 204 context parameter and TCP framing because its replies exceed the UDP amplification budget. It is not interoperable with standard Roughtime implementations.

Build

Go 1.27 or newer is required.

make build

This produces roughtime, roughtime-client, roughtime-debug, roughtime-bench, and roughtime-stamp. Run any command with -h for its complete flag list.

Server

The server accepts IETF Ed25519 requests over UDP and TCP and Google-Roughtime over UDP. The experimental ML-DSA-44 suite uses TCP only. The Linux UDP path uses one SO_REUSEPORT socket per GOMAXPROCS worker and batched I/O. OpenBSD 7.2 or later is required, for its recvmmsg/sendmmsg. Batching on OpenBSD amortizes syscalls but still signs on one goroutine per socket, and because OpenBSD has no IPv4-mapped IPv6 a wildcard bind gets one socket per address family. Other Unix systems use a portable socket loop. Windows is not supported.

Generate a root key and start the server:

roughtime -keygen /path/to/root.key
roughtime -root-key-file /path/to/root.key

Use -pq-keygen and -pq-root-key-file for ML-DSA-44, or configure both root files. Seed-file permissions are no broader than 0600; existing files are never overwritten. Online delegation certificates refresh automatically. With -offline-delegation, root files are read only during startup and the server stops when the delegation leaves its validity window.

By default the server greases 1% of responses to exercise client error paths; set -grease-rate 0 when deterministic replies are required.

UDP and TCP share -port (default 2002). On multihomed hosts, -listen-address binds both listeners to the advertised local address so UDP replies retain that source. -metrics-addr enables unauthenticated Prometheus /metrics and /healthz endpoints; bind it to loopback unless an external access-control layer protects it.

Docker

docker build -t roughtime .
mkdir -p keys
docker run --rm --user "$(id -u):$(id -g)" -v "$PWD/keys:/keys" \
roughtime -keygen /keys/root.key
docker run --read-only --user "$(id -u):$(id -g)" \
--cap-drop ALL --security-opt no-new-privileges \
-p 2002:2002/udp -p 2002:2002/tcp -v "$PWD/keys:/keys:ro" \
roughtime -root-key-file /keys/root.key

The example uses the host user so the generated private key remains readable. Without --user, the runtime image uses UID 65532 and mounted files must be readable by that UID.

Commands

roughtime-client

Query one server with -addr and -pubkey, or an ecosystem with -servers. Multi-server ecosystem queries form a causal chain unless -chain=false is set. The default samples up to five endpoint-domain groups; -all disables sampling and queries every transport-compatible entry. That grouping is a diversity heuristic, not authenticated operator identity or Sybil resistance.

go run ./cmd/roughtime-client -servers ecosystem.json -all

roughtime-debug

Probe supported versions and inspect authenticated response structure and timing data. Draft 12 is tried in typed and untyped forms.

go run ./cmd/roughtime-debug -addr time.txryan.com:2002 \
-pubkey iBVjxg/1j7y1+kQUTBYdTabxCppesU/07D4PMDJk2WA=

roughtime-bench

A closed-loop load generator intended for servers you control. -verify checks each signature and Merkle proof; without it, results measure transport only and may count malformed replies.

go run ./cmd/roughtime-bench -addr 127.0.0.1:2002 -pubkey <key> \
-workers 64 -duration 10s -verify

roughtime-stamp

Create or verify a document timestamp receipt. New receipts select three compatible endpoint-domain groups and query them twice in the same order. The document is hashed before querying and again immediately before the proof is durably persisted. Verification checks the document, the full causal chain, and the trusted ecosystem. Existing one-pass proofs remain readable and are reported as legacy receipts.

go run ./cmd/roughtime-stamp -doc README.md -servers ecosystem.json \
-out README.md.proof
go run ./cmd/roughtime-stamp -mode verify -doc README.md \
-servers ecosystem.json -in README.md.proof

Go API

The top-level package provides Client.Query, concurrent QueryAll, causal QueryChain, document-bound QueryChainWithNonce, consensus helpers, proof serialization and offline verification, and ecosystem parsing.

pk, err:=roughtime.DecodePublicKey(encodedKey)
iferr!=nil {
returnerr
}
server:= roughtime.Server{
Name: "example",
PublicKey: pk,
Addresses: []roughtime.Address{{
Transport: "udp",
Address: "example.com:2002",
}},
}
response, err:=new(roughtime.Client).Query(ctx, server)

The protocol package exposes request parsing/building, reply creation and verification, transports, version negotiation, chaining, and malfeasance reports.

Compatibility details for low-level callers:

  • IETF request builders use a 1024-byte body; ML-DSA-44 uses 8192 bytes. The 12-byte ROUGHTIM frame is additional.
  • RequestOptions.LegacyPacketSize produces the historical 1024/8192-byte total packet size required by some deployed peers; the high-level client enables it for interoperability.
  • RequestOptions.OmitTYPE produces the untyped drafts 12/13 request.
  • VerifyOptions.RequireTYPE requires the draft-14+ typed exchange. The default verifier accepts both forms.
  • ReplyOptions.Draft14NodeFirst selects the node-first Merkle convention from drafts 14–15. The default builder remains hash-first for backward compatibility; verification accepts both node-first and draft-16+ hash-first proofs.
  • NewCertificateWithVersions binds the signed VERS list to a server's actual advertised versions.

ComputeSRV(rootPublicKey) returns the drafts 10+ server binding. Verifiers check negotiated versions, signed VERS, SRV, delegation validity, signatures, nonces, timestamps, and Merkle proofs.

Development

make deps # install development tools once
make test# tests
make test-race # race detector
make fuzz # retained parser/verifier fuzzers; FUZZ_TIME defaults to 30s
make lint # go vet and staticcheck
make vuln # reachable-vulnerability scan
make check # dependency, vendor, format, lint, security, build, race

The vendor tree is excluded from source edits and is checked for reproducibility with make verify-vendor.

License

Copyright (c) 2026 Tanner Ryan. All rights reserved. Use of this source code is governed by the BSD 2-Clause License.

About

Roughtime in Go: Google-Roughtime, IETF drafts 01-19, experimental post-quantum ML-DSA-44, and a production-ready server with automatic certificate refresh.

Topics

Resources

Contributing

Security policy

Stars

8 stars

Watchers

1 watching

Forks

Releases

Used by

Contributors

Languages

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

Repository files navigation

roughtime

Go ReferenceLicense

A Go implementation of Google-Roughtime and IETF Roughtime drafts 01–19. The repository contains a high-level client package, the low-level protocol package, a high-throughput server, and client, debug, benchmark, and document-stamping commands.

Drafts 12–19 share wire version 0x8000000c. Drafts 14–19 add the TYPE exchange without changing that value, so clients must support both typed and untyped peers.

Try the public server:

go run ./cmd/roughtime-client -addr time.txryan.com:2002 \
-pubkey iBVjxg/1j7y1+kQUTBYdTabxCppesU/07D4PMDJk2WA=

ML-DSA-44 support is an experimental, non-IETF extension. It uses the FIPS 204 context parameter and TCP framing because its replies exceed the UDP amplification budget. It is not interoperable with standard Roughtime implementations.

Build

Go 1.27 or newer is required.

make build

This produces roughtime, roughtime-client, roughtime-debug, roughtime-bench, and roughtime-stamp. Run any command with -h for its complete flag list.

Server

The server accepts IETF Ed25519 requests over UDP and TCP and Google-Roughtime over UDP. The experimental ML-DSA-44 suite uses TCP only. The Linux UDP path uses one SO_REUSEPORT socket per GOMAXPROCS worker and batched I/O. OpenBSD 7.2 or later is required, for its recvmmsg/sendmmsg. Batching on OpenBSD amortizes syscalls but still signs on one goroutine per socket, and because OpenBSD has no IPv4-mapped IPv6 a wildcard bind gets one socket per address family. Other Unix systems use a portable socket loop. Windows is not supported.

Generate a root key and start the server:

roughtime -keygen /path/to/root.key
roughtime -root-key-file /path/to/root.key

Use -pq-keygen and -pq-root-key-file for ML-DSA-44, or configure both root files. Seed-file permissions are no broader than 0600; existing files are never overwritten. Online delegation certificates refresh automatically. With -offline-delegation, root files are read only during startup and the server stops when the delegation leaves its validity window.

By default the server greases 1% of responses to exercise client error paths; set -grease-rate 0 when deterministic replies are required.

UDP and TCP share -port (default 2002). On multihomed hosts, -listen-address binds both listeners to the advertised local address so UDP replies retain that source. -metrics-addr enables unauthenticated Prometheus /metrics and /healthz endpoints; bind it to loopback unless an external access-control layer protects it.

Docker

docker build -t roughtime .
mkdir -p keys
docker run --rm --user "$(id -u):$(id -g)" -v "$PWD/keys:/keys" \
roughtime -keygen /keys/root.key
docker run --read-only --user "$(id -u):$(id -g)" \
--cap-drop ALL --security-opt no-new-privileges \
-p 2002:2002/udp -p 2002:2002/tcp -v "$PWD/keys:/keys:ro" \
roughtime -root-key-file /keys/root.key

The example uses the host user so the generated private key remains readable. Without --user, the runtime image uses UID 65532 and mounted files must be readable by that UID.

Commands

roughtime-client

Query one server with -addr and -pubkey, or an ecosystem with -servers. Multi-server ecosystem queries form a causal chain unless -chain=false is set. The default samples up to five endpoint-domain groups; -all disables sampling and queries every transport-compatible entry. That grouping is a diversity heuristic, not authenticated operator identity or Sybil resistance.

go run ./cmd/roughtime-client -servers ecosystem.json -all

roughtime-debug

Probe supported versions and inspect authenticated response structure and timing data. Draft 12 is tried in typed and untyped forms.

go run ./cmd/roughtime-debug -addr time.txryan.com:2002 \
-pubkey iBVjxg/1j7y1+kQUTBYdTabxCppesU/07D4PMDJk2WA=

roughtime-bench

A closed-loop load generator intended for servers you control. -verify checks each signature and Merkle proof; without it, results measure transport only and may count malformed replies.

go run ./cmd/roughtime-bench -addr 127.0.0.1:2002 -pubkey <key> \
-workers 64 -duration 10s -verify

roughtime-stamp

Create or verify a document timestamp receipt. New receipts select three compatible endpoint-domain groups and query them twice in the same order. The document is hashed before querying and again immediately before the proof is durably persisted. Verification checks the document, the full causal chain, and the trusted ecosystem. Existing one-pass proofs remain readable and are reported as legacy receipts.

go run ./cmd/roughtime-stamp -doc README.md -servers ecosystem.json \
-out README.md.proof
go run ./cmd/roughtime-stamp -mode verify -doc README.md \
-servers ecosystem.json -in README.md.proof

Go API

The top-level package provides Client.Query, concurrent QueryAll, causal QueryChain, document-bound QueryChainWithNonce, consensus helpers, proof serialization and offline verification, and ecosystem parsing.

pk, err:=roughtime.DecodePublicKey(encodedKey)
iferr!=nil {
returnerr
}
server:= roughtime.Server{
Name: "example",
PublicKey: pk,
Addresses: []roughtime.Address{{
Transport: "udp",
Address: "example.com:2002",
}},
}
response, err:=new(roughtime.Client).Query(ctx, server)

The protocol package exposes request parsing/building, reply creation and verification, transports, version negotiation, chaining, and malfeasance reports.

Compatibility details for low-level callers:

  • IETF request builders use a 1024-byte body; ML-DSA-44 uses 8192 bytes. The 12-byte ROUGHTIM frame is additional.
  • RequestOptions.LegacyPacketSize produces the historical 1024/8192-byte total packet size required by some deployed peers; the high-level client enables it for interoperability.
  • RequestOptions.OmitTYPE produces the untyped drafts 12/13 request.
  • VerifyOptions.RequireTYPE requires the draft-14+ typed exchange. The default verifier accepts both forms.
  • ReplyOptions.Draft14NodeFirst selects the node-first Merkle convention from drafts 14–15. The default builder remains hash-first for backward compatibility; verification accepts both node-first and draft-16+ hash-first proofs.
  • NewCertificateWithVersions binds the signed VERS list to a server's actual advertised versions.

ComputeSRV(rootPublicKey) returns the drafts 10+ server binding. Verifiers check negotiated versions, signed VERS, SRV, delegation validity, signatures, nonces, timestamps, and Merkle proofs.

Development

make deps # install development tools once
make test# tests
make test-race # race detector
make fuzz # retained parser/verifier fuzzers; FUZZ_TIME defaults to 30s
make lint # go vet and staticcheck
make vuln # reachable-vulnerability scan
make check # dependency, vendor, format, lint, security, build, race

The vendor tree is excluded from source edits and is checked for reproducibility with make verify-vendor.

License

Copyright (c) 2026 Tanner Ryan. All rights reserved. Use of this source code is governed by the BSD 2-Clause License.

About

Roughtime in Go: Google-Roughtime, IETF drafts 01-19, experimental post-quantum ML-DSA-44, and a production-ready server with automatic certificate refresh.

Topics

Resources

Contributing

Security policy

Stars

8 stars

Watchers

1 watching

Forks

Releases

Used by

Contributors

Languages

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

Repository files navigation

roughtime

Go ReferenceLicense

A Go implementation of Google-Roughtime and IETF Roughtime drafts 01–19. The repository contains a high-level client package, the low-level protocol package, a high-throughput server, and client, debug, benchmark, and document-stamping commands.

Drafts 12–19 share wire version 0x8000000c. Drafts 14–19 add the TYPE exchange without changing that value, so clients must support both typed and untyped peers.

Try the public server:

go run ./cmd/roughtime-client -addr time.txryan.com:2002 \
-pubkey iBVjxg/1j7y1+kQUTBYdTabxCppesU/07D4PMDJk2WA=

ML-DSA-44 support is an experimental, non-IETF extension. It uses the FIPS 204 context parameter and TCP framing because its replies exceed the UDP amplification budget. It is not interoperable with standard Roughtime implementations.

Build

Go 1.27 or newer is required.

make build

This produces roughtime, roughtime-client, roughtime-debug, roughtime-bench, and roughtime-stamp. Run any command with -h for its complete flag list.

Server

The server accepts IETF Ed25519 requests over UDP and TCP and Google-Roughtime over UDP. The experimental ML-DSA-44 suite uses TCP only. The Linux UDP path uses one SO_REUSEPORT socket per GOMAXPROCS worker and batched I/O. OpenBSD 7.2 or later is required, for its recvmmsg/sendmmsg. Batching on OpenBSD amortizes syscalls but still signs on one goroutine per socket, and because OpenBSD has no IPv4-mapped IPv6 a wildcard bind gets one socket per address family. Other Unix systems use a portable socket loop. Windows is not supported.

Generate a root key and start the server:

roughtime -keygen /path/to/root.key
roughtime -root-key-file /path/to/root.key

Use -pq-keygen and -pq-root-key-file for ML-DSA-44, or configure both root files. Seed-file permissions are no broader than 0600; existing files are never overwritten. Online delegation certificates refresh automatically. With -offline-delegation, root files are read only during startup and the server stops when the delegation leaves its validity window.

By default the server greases 1% of responses to exercise client error paths; set -grease-rate 0 when deterministic replies are required.

UDP and TCP share -port (default 2002). On multihomed hosts, -listen-address binds both listeners to the advertised local address so UDP replies retain that source. -metrics-addr enables unauthenticated Prometheus /metrics and /healthz endpoints; bind it to loopback unless an external access-control layer protects it.

Docker

docker build -t roughtime .
mkdir -p keys
docker run --rm --user "$(id -u):$(id -g)" -v "$PWD/keys:/keys" \
roughtime -keygen /keys/root.key
docker run --read-only --user "$(id -u):$(id -g)" \
--cap-drop ALL --security-opt no-new-privileges \
-p 2002:2002/udp -p 2002:2002/tcp -v "$PWD/keys:/keys:ro" \
roughtime -root-key-file /keys/root.key

The example uses the host user so the generated private key remains readable. Without --user, the runtime image uses UID 65532 and mounted files must be readable by that UID.

Commands

roughtime-client

Query one server with -addr and -pubkey, or an ecosystem with -servers. Multi-server ecosystem queries form a causal chain unless -chain=false is set. The default samples up to five endpoint-domain groups; -all disables sampling and queries every transport-compatible entry. That grouping is a diversity heuristic, not authenticated operator identity or Sybil resistance.

go run ./cmd/roughtime-client -servers ecosystem.json -all

roughtime-debug

Probe supported versions and inspect authenticated response structure and timing data. Draft 12 is tried in typed and untyped forms.

go run ./cmd/roughtime-debug -addr time.txryan.com:2002 \
-pubkey iBVjxg/1j7y1+kQUTBYdTabxCppesU/07D4PMDJk2WA=

roughtime-bench

A closed-loop load generator intended for servers you control. -verify checks each signature and Merkle proof; without it, results measure transport only and may count malformed replies.

go run ./cmd/roughtime-bench -addr 127.0.0.1:2002 -pubkey <key> \
-workers 64 -duration 10s -verify

roughtime-stamp

Create or verify a document timestamp receipt. New receipts select three compatible endpoint-domain groups and query them twice in the same order. The document is hashed before querying and again immediately before the proof is durably persisted. Verification checks the document, the full causal chain, and the trusted ecosystem. Existing one-pass proofs remain readable and are reported as legacy receipts.

go run ./cmd/roughtime-stamp -doc README.md -servers ecosystem.json \
-out README.md.proof
go run ./cmd/roughtime-stamp -mode verify -doc README.md \
-servers ecosystem.json -in README.md.proof

Go API

The top-level package provides Client.Query, concurrent QueryAll, causal QueryChain, document-bound QueryChainWithNonce, consensus helpers, proof serialization and offline verification, and ecosystem parsing.

pk, err:=roughtime.DecodePublicKey(encodedKey)
iferr!=nil {
returnerr
}
server:= roughtime.Server{
Name: "example",
PublicKey: pk,
Addresses: []roughtime.Address{{
Transport: "udp",
Address: "example.com:2002",
}},
}
response, err:=new(roughtime.Client).Query(ctx, server)

The protocol package exposes request parsing/building, reply creation and verification, transports, version negotiation, chaining, and malfeasance reports.

Compatibility details for low-level callers:

  • IETF request builders use a 1024-byte body; ML-DSA-44 uses 8192 bytes. The 12-byte ROUGHTIM frame is additional.
  • RequestOptions.LegacyPacketSize produces the historical 1024/8192-byte total packet size required by some deployed peers; the high-level client enables it for interoperability.
  • RequestOptions.OmitTYPE produces the untyped drafts 12/13 request.
  • VerifyOptions.RequireTYPE requires the draft-14+ typed exchange. The default verifier accepts both forms.
  • ReplyOptions.Draft14NodeFirst selects the node-first Merkle convention from drafts 14–15. The default builder remains hash-first for backward compatibility; verification accepts both node-first and draft-16+ hash-first proofs.
  • NewCertificateWithVersions binds the signed VERS list to a server's actual advertised versions.

ComputeSRV(rootPublicKey) returns the drafts 10+ server binding. Verifiers check negotiated versions, signed VERS, SRV, delegation validity, signatures, nonces, timestamps, and Merkle proofs.

Development

make deps # install development tools once
make test# tests
make test-race # race detector
make fuzz # retained parser/verifier fuzzers; FUZZ_TIME defaults to 30s
make lint # go vet and staticcheck
make vuln # reachable-vulnerability scan
make check # dependency, vendor, format, lint, security, build, race

The vendor tree is excluded from source edits and is checked for reproducibility with make verify-vendor.

License

Copyright (c) 2026 Tanner Ryan. All rights reserved. Use of this source code is governed by the BSD 2-Clause License.

About

Roughtime in Go: Google-Roughtime, IETF drafts 01-19, experimental post-quantum ML-DSA-44, and a production-ready server with automatic certificate refresh.

Topics

Resources

Contributing

Security policy

Stars

8 stars

Watchers

1 watching

Forks

Releases

Used by

Contributors

Languages

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

Repository files navigation

roughtime

Go ReferenceLicense

A Go implementation of Google-Roughtime and IETF Roughtime drafts 01–19. The repository contains a high-level client package, the low-level protocol package, a high-throughput server, and client, debug, benchmark, and document-stamping commands.

Drafts 12–19 share wire version 0x8000000c. Drafts 14–19 add the TYPE exchange without changing that value, so clients must support both typed and untyped peers.

Try the public server:

go run ./cmd/roughtime-client -addr time.txryan.com:2002 \
-pubkey iBVjxg/1j7y1+kQUTBYdTabxCppesU/07D4PMDJk2WA=

ML-DSA-44 support is an experimental, non-IETF extension. It uses the FIPS 204 context parameter and TCP framing because its replies exceed the UDP amplification budget. It is not interoperable with standard Roughtime implementations.

Build

Go 1.27 or newer is required.

make build

This produces roughtime, roughtime-client, roughtime-debug, roughtime-bench, and roughtime-stamp. Run any command with -h for its complete flag list.

Server

The server accepts IETF Ed25519 requests over UDP and TCP and Google-Roughtime over UDP. The experimental ML-DSA-44 suite uses TCP only. The Linux UDP path uses one SO_REUSEPORT socket per GOMAXPROCS worker and batched I/O. OpenBSD 7.2 or later is required, for its recvmmsg/sendmmsg. Batching on OpenBSD amortizes syscalls but still signs on one goroutine per socket, and because OpenBSD has no IPv4-mapped IPv6 a wildcard bind gets one socket per address family. Other Unix systems use a portable socket loop. Windows is not supported.

Generate a root key and start the server:

roughtime -keygen /path/to/root.key
roughtime -root-key-file /path/to/root.key

Use -pq-keygen and -pq-root-key-file for ML-DSA-44, or configure both root files. Seed-file permissions are no broader than 0600; existing files are never overwritten. Online delegation certificates refresh automatically. With -offline-delegation, root files are read only during startup and the server stops when the delegation leaves its validity window.

By default the server greases 1% of responses to exercise client error paths; set -grease-rate 0 when deterministic replies are required.

UDP and TCP share -port (default 2002). On multihomed hosts, -listen-address binds both listeners to the advertised local address so UDP replies retain that source. -metrics-addr enables unauthenticated Prometheus /metrics and /healthz endpoints; bind it to loopback unless an external access-control layer protects it.

Docker

docker build -t roughtime .
mkdir -p keys
docker run --rm --user "$(id -u):$(id -g)" -v "$PWD/keys:/keys" \
roughtime -keygen /keys/root.key
docker run --read-only --user "$(id -u):$(id -g)" \
--cap-drop ALL --security-opt no-new-privileges \
-p 2002:2002/udp -p 2002:2002/tcp -v "$PWD/keys:/keys:ro" \
roughtime -root-key-file /keys/root.key

The example uses the host user so the generated private key remains readable. Without --user, the runtime image uses UID 65532 and mounted files must be readable by that UID.

Commands

roughtime-client

Query one server with -addr and -pubkey, or an ecosystem with -servers. Multi-server ecosystem queries form a causal chain unless -chain=false is set. The default samples up to five endpoint-domain groups; -all disables sampling and queries every transport-compatible entry. That grouping is a diversity heuristic, not authenticated operator identity or Sybil resistance.

go run ./cmd/roughtime-client -servers ecosystem.json -all

roughtime-debug

Probe supported versions and inspect authenticated response structure and timing data. Draft 12 is tried in typed and untyped forms.

go run ./cmd/roughtime-debug -addr time.txryan.com:2002 \
-pubkey iBVjxg/1j7y1+kQUTBYdTabxCppesU/07D4PMDJk2WA=

roughtime-bench

A closed-loop load generator intended for servers you control. -verify checks each signature and Merkle proof; without it, results measure transport only and may count malformed replies.

go run ./cmd/roughtime-bench -addr 127.0.0.1:2002 -pubkey <key> \
-workers 64 -duration 10s -verify

roughtime-stamp

Create or verify a document timestamp receipt. New receipts select three compatible endpoint-domain groups and query them twice in the same order. The document is hashed before querying and again immediately before the proof is durably persisted. Verification checks the document, the full causal chain, and the trusted ecosystem. Existing one-pass proofs remain readable and are reported as legacy receipts.

go run ./cmd/roughtime-stamp -doc README.md -servers ecosystem.json \
-out README.md.proof
go run ./cmd/roughtime-stamp -mode verify -doc README.md \
-servers ecosystem.json -in README.md.proof

Go API

The top-level package provides Client.Query, concurrent QueryAll, causal QueryChain, document-bound QueryChainWithNonce, consensus helpers, proof serialization and offline verification, and ecosystem parsing.

pk, err:=roughtime.DecodePublicKey(encodedKey)
iferr!=nil {
returnerr
}
server:= roughtime.Server{
Name: "example",
PublicKey: pk,
Addresses: []roughtime.Address{{
Transport: "udp",
Address: "example.com:2002",
}},
}
response, err:=new(roughtime.Client).Query(ctx, server)

The protocol package exposes request parsing/building, reply creation and verification, transports, version negotiation, chaining, and malfeasance reports.

Compatibility details for low-level callers:

  • IETF request builders use a 1024-byte body; ML-DSA-44 uses 8192 bytes. The 12-byte ROUGHTIM frame is additional.
  • RequestOptions.LegacyPacketSize produces the historical 1024/8192-byte total packet size required by some deployed peers; the high-level client enables it for interoperability.
  • RequestOptions.OmitTYPE produces the untyped drafts 12/13 request.
  • VerifyOptions.RequireTYPE requires the draft-14+ typed exchange. The default verifier accepts both forms.
  • ReplyOptions.Draft14NodeFirst selects the node-first Merkle convention from drafts 14–15. The default builder remains hash-first for backward compatibility; verification accepts both node-first and draft-16+ hash-first proofs.
  • NewCertificateWithVersions binds the signed VERS list to a server's actual advertised versions.

ComputeSRV(rootPublicKey) returns the drafts 10+ server binding. Verifiers check negotiated versions, signed VERS, SRV, delegation validity, signatures, nonces, timestamps, and Merkle proofs.

Development

make deps # install development tools once
make test# tests
make test-race # race detector
make fuzz # retained parser/verifier fuzzers; FUZZ_TIME defaults to 30s
make lint # go vet and staticcheck
make vuln # reachable-vulnerability scan
make check # dependency, vendor, format, lint, security, build, race

The vendor tree is excluded from source edits and is checked for reproducibility with make verify-vendor.

License

Copyright (c) 2026 Tanner Ryan. All rights reserved. Use of this source code is governed by the BSD 2-Clause License.

About

Roughtime in Go: Google-Roughtime, IETF drafts 01-19, experimental post-quantum ML-DSA-44, and a production-ready server with automatic certificate refresh.

Topics

Resources

Contributing

Security policy

Stars

8 stars

Watchers

1 watching

Forks

Releases

Used by

Contributors

Languages

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

Repository files navigation

roughtime

Go ReferenceLicense

A Go implementation of Google-Roughtime and IETF Roughtime drafts 01–19. The repository contains a high-level client package, the low-level protocol package, a high-throughput server, and client, debug, benchmark, and document-stamping commands.

Drafts 12–19 share wire version 0x8000000c. Drafts 14–19 add the TYPE exchange without changing that value, so clients must support both typed and untyped peers.

Try the public server:

go run ./cmd/roughtime-client -addr time.txryan.com:2002 \
-pubkey iBVjxg/1j7y1+kQUTBYdTabxCppesU/07D4PMDJk2WA=

ML-DSA-44 support is an experimental, non-IETF extension. It uses the FIPS 204 context parameter and TCP framing because its replies exceed the UDP amplification budget. It is not interoperable with standard Roughtime implementations.

Build

Go 1.27 or newer is required.

make build

This produces roughtime, roughtime-client, roughtime-debug, roughtime-bench, and roughtime-stamp. Run any command with -h for its complete flag list.

Server

The server accepts IETF Ed25519 requests over UDP and TCP and Google-Roughtime over UDP. The experimental ML-DSA-44 suite uses TCP only. The Linux UDP path uses one SO_REUSEPORT socket per GOMAXPROCS worker and batched I/O. OpenBSD 7.2 or later is required, for its recvmmsg/sendmmsg. Batching on OpenBSD amortizes syscalls but still signs on one goroutine per socket, and because OpenBSD has no IPv4-mapped IPv6 a wildcard bind gets one socket per address family. Other Unix systems use a portable socket loop. Windows is not supported.

Generate a root key and start the server:

roughtime -keygen /path/to/root.key
roughtime -root-key-file /path/to/root.key

Use -pq-keygen and -pq-root-key-file for ML-DSA-44, or configure both root files. Seed-file permissions are no broader than 0600; existing files are never overwritten. Online delegation certificates refresh automatically. With -offline-delegation, root files are read only during startup and the server stops when the delegation leaves its validity window.

By default the server greases 1% of responses to exercise client error paths; set -grease-rate 0 when deterministic replies are required.

UDP and TCP share -port (default 2002). On multihomed hosts, -listen-address binds both listeners to the advertised local address so UDP replies retain that source. -metrics-addr enables unauthenticated Prometheus /metrics and /healthz endpoints; bind it to loopback unless an external access-control layer protects it.

Docker

docker build -t roughtime .
mkdir -p keys
docker run --rm --user "$(id -u):$(id -g)" -v "$PWD/keys:/keys" \
roughtime -keygen /keys/root.key
docker run --read-only --user "$(id -u):$(id -g)" \
--cap-drop ALL --security-opt no-new-privileges \
-p 2002:2002/udp -p 2002:2002/tcp -v "$PWD/keys:/keys:ro" \
roughtime -root-key-file /keys/root.key

The example uses the host user so the generated private key remains readable. Without --user, the runtime image uses UID 65532 and mounted files must be readable by that UID.

Commands

roughtime-client

Query one server with -addr and -pubkey, or an ecosystem with -servers. Multi-server ecosystem queries form a causal chain unless -chain=false is set. The default samples up to five endpoint-domain groups; -all disables sampling and queries every transport-compatible entry. That grouping is a diversity heuristic, not authenticated operator identity or Sybil resistance.

go run ./cmd/roughtime-client -servers ecosystem.json -all

roughtime-debug

Probe supported versions and inspect authenticated response structure and timing data. Draft 12 is tried in typed and untyped forms.

go run ./cmd/roughtime-debug -addr time.txryan.com:2002 \
-pubkey iBVjxg/1j7y1+kQUTBYdTabxCppesU/07D4PMDJk2WA=

roughtime-bench

A closed-loop load generator intended for servers you control. -verify checks each signature and Merkle proof; without it, results measure transport only and may count malformed replies.

go run ./cmd/roughtime-bench -addr 127.0.0.1:2002 -pubkey <key> \
-workers 64 -duration 10s -verify

roughtime-stamp

Create or verify a document timestamp receipt. New receipts select three compatible endpoint-domain groups and query them twice in the same order. The document is hashed before querying and again immediately before the proof is durably persisted. Verification checks the document, the full causal chain, and the trusted ecosystem. Existing one-pass proofs remain readable and are reported as legacy receipts.

go run ./cmd/roughtime-stamp -doc README.md -servers ecosystem.json \
-out README.md.proof
go run ./cmd/roughtime-stamp -mode verify -doc README.md \
-servers ecosystem.json -in README.md.proof

Go API

The top-level package provides Client.Query, concurrent QueryAll, causal QueryChain, document-bound QueryChainWithNonce, consensus helpers, proof serialization and offline verification, and ecosystem parsing.

pk, err:=roughtime.DecodePublicKey(encodedKey)
iferr!=nil {
returnerr
}
server:= roughtime.Server{
Name: "example",
PublicKey: pk,
Addresses: []roughtime.Address{{
Transport: "udp",
Address: "example.com:2002",
}},
}
response, err:=new(roughtime.Client).Query(ctx, server)

The protocol package exposes request parsing/building, reply creation and verification, transports, version negotiation, chaining, and malfeasance reports.

Compatibility details for low-level callers:

  • IETF request builders use a 1024-byte body; ML-DSA-44 uses 8192 bytes. The 12-byte ROUGHTIM frame is additional.
  • RequestOptions.LegacyPacketSize produces the historical 1024/8192-byte total packet size required by some deployed peers; the high-level client enables it for interoperability.
  • RequestOptions.OmitTYPE produces the untyped drafts 12/13 request.
  • VerifyOptions.RequireTYPE requires the draft-14+ typed exchange. The default verifier accepts both forms.
  • ReplyOptions.Draft14NodeFirst selects the node-first Merkle convention from drafts 14–15. The default builder remains hash-first for backward compatibility; verification accepts both node-first and draft-16+ hash-first proofs.
  • NewCertificateWithVersions binds the signed VERS list to a server's actual advertised versions.

ComputeSRV(rootPublicKey) returns the drafts 10+ server binding. Verifiers check negotiated versions, signed VERS, SRV, delegation validity, signatures, nonces, timestamps, and Merkle proofs.

Development

make deps # install development tools once
make test# tests
make test-race # race detector
make fuzz # retained parser/verifier fuzzers; FUZZ_TIME defaults to 30s
make lint # go vet and staticcheck
make vuln # reachable-vulnerability scan
make check # dependency, vendor, format, lint, security, build, race

The vendor tree is excluded from source edits and is checked for reproducibility with make verify-vendor.

License

Copyright (c) 2026 Tanner Ryan. All rights reserved. Use of this source code is governed by the BSD 2-Clause License.

About

Roughtime in Go: Google-Roughtime, IETF drafts 01-19, experimental post-quantum ML-DSA-44, and a production-ready server with automatic certificate refresh.

Topics

Resources

Contributing

Security policy

Stars

8 stars

Watchers

1 watching

Forks

Releases

Used by

Contributors

Languages