A Rust implementation of the Static CT API for deployment on Cloudflare Workers.
This project can be used to run multiple CT log shards within a single Workers application.
The 'brain' of each CT log is a single-threaded 'Sequencer' Durable Object, and much of the system is architected around offloading as much work as possible to other components of the system (like 'Batcher' Durable Objects) to improve overall throughput. Read the blog post for more details.
⚠️ Warning The software in this crate is written specifically for the Cloudflare Durable Objects execution model, with single-threaded execution and input/output gates to avoid race conditions. Running it elsewhere could lead to concurrency bugs.
The Frontend (a Worker in a location close to the client) handles incoming requests (1) for the Submission APIs. After validating the request (2) and checking the deduplication cache (3), it submits the entry (4) to a Batcher (selected via consistent hashing over the entry), and awaits the response.
The Batcher receives requests (keeping them open) and groups the entries into batches which it submits (5) to the Sequencer, which then adds the request to a pool of entries to be sequenced. An Alarm fires every sequence_interval (default 1s) to trigger the Sequencer to sequence the pool of entries (6) and update state in the Object (R2) and Lock (Durable Object Storage) backends.
After persisting log state, the Sequencer returns sequenced entry metadata (7) to the Batcher, which in turn sends entry metadata to waiting Frontend requests and writes batch metadata to the deduplication cache in Workers KV. When the Frontend receives the response, it returns a Signed Certificate Timestamp (SCT) to the client (8).
Two prototype logs are available for testing, with configuration in wrangler.jsonc and config.cftest.json and roots from roots.default.pem.
curl -s https://static-ct.cloudflareresearch.com/logs/cftest2025h1a/metadata | jq
{
"description": "Cloudflare Research 'cftest2025h1a' log",
"log_type": "test",
"log_id": "7DSwkhPo35hYEZa4DVlPq6Pm/bG4aOw/kqhHvYd6z/k=",
"key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8LxK0sAKYODiZe9gDeak7agggQ0wvBOeEMSi7cLlFzcTlm1AexxsC04r/4rBIhf8liQqyRTrL3u1jpz6NJ4tLg==",
"witness_key": "MCowBQYDK2VwAyEAWTVSsOnsIYq+LZ6CUxgI8ONvJvE+YSF27N9BXZ02EP8=",
"mmd": 86400,
"submission_url": "https://static-ct.cloudflareresearch.com/logs/cftest2025h1a/",
"monitoring_url": "https://static-ct-public-cftest2025h1a.cloudflareresearch.com/",
"temporal_interval": {
"start_inclusive": "2025-01-01T00:00:00Z",
"end_exclusive": "2025-07-01T00:00:00Z"
}
}
curl -s https://static-ct.cloudflareresearch.com/logs/cftest2025h2a/metadata | jq
{
"description": "Cloudflare Research 'cftest2025h2a' log",
"log_type": "test",
"log_id": "2KJiliJSBM2181NJWC5O1mWiRRsPJ6i2iWE2s7n8Bwg=",
"key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEYipauBOPEktPb0JVpkRQq6wtRDRIj8GmKYvzM0Lpw1oSh9Uis9khpPCH6xyrDstk019AHuCq19KT5f+/MkY/yA==",
"witness_key": "MCowBQYDK2VwAyEA8jhNnqw2LXtyjb0Os+R3eiKfxnsP8tnke5iZZ16nBbU=",
"mmd": 86400,
"submission_url": "https://static-ct.cloudflareresearch.com/logs/cftest2025h2a/",
"monitoring_url": "https://static-ct-public-cftest2025h2a.cloudflareresearch.com/",
"temporal_interval": {
"start_inclusive": "2025-07-01T00:00:00Z",
"end_exclusive": "2026-01-01T00:00:00Z"
}
}
Follow these instructions to spin up a CT log on your local machine using the dev configuration in wrangler.jsonc and config.dev.json (schema at config.schema.json), and secrets in .dev.vars.
-
(Optional) Clear the local storage cache:
./reset-dev.sh -
Deploy worker locally with
npx wrangler -e=dev dev. -
Send some requests. After the first request that hits the Durable Object (
/ct/v1/add-[pre-]chainor/metrics), the sequencing loop will begin.Submit a certificate from
google.comby runningpython3 scripts/add_cert_to_local_dev.py. Note you need Python 3.13 or greater.Use ctclient to 'cross-pollinate' entries from another log (RFC6962 logs only, until static-ct-api support is added) with overlapping roots and
NotAftertemporal interval:tmpdir=$(mktemp -d) ./ctclient get-entries --first 0 --last 31 --log_name "Google 'Argon2025h1' log" --chain --text=false | csplit -s -f $tmpdir/ - '/^Index=/' '{30}' for file in $tmpdir/*; do prefix=$(head -n1 $file | grep -o "pre-") cat $file | while (set -o pipefail; openssl x509 -outform DER 2>/dev/null | base64); do :; done |\ sed '/^$/d' | sed 's/.*/"&"/' | jq -sc '{"chain":.}' |\ curl -s "http://localhost:8787/logs/dev2025h1a/ct/v1/add-${prefix}chain" -d@- & done rm -r $tmpdirCheckpoints and other static data can also be retrieved through the worker (or directly from the R2 bucket):
curl -s "http://localhost:8787/logs/dev2025h1a/checkpoint"Metadata necessary for writing to or consuming from logs is available at /metadata.
curl -s "http://localhost:8787/logs/dev2025h1a/metadata"Prometheus metrics are exposed publicly at /metrics.
curl -s "http://localhost:8787/logs/dev2025h1a/metrics"
Follow these instructions to deploy a CT log with the dev configuration to Cloudflare's network.
Run the following for each of the dev2025h1a and dev2025h2a log shards to configure resources (or use scripts/create-log.sh):
- Set log shard name and deployment environment. The [location hint][location-hint] is optional.
export LOG_NAME=dev2025h1a
export CLOUDFLARE_ACCOUNT_ID=some-account-id-here
export ENV=dev
export LOCATION=wnam # optional- Setup the roots kv namespace
npx wrangler -e="${ENV}" kv namespace create static-ct-ccadb-roots --binding ccadb_rootsAlternatively run the script create-root-kv.sh
- Create the the R2 bucket for public assets, the kv namespace for per-log deduplication cache and generate the [secrets][secrets-docs] for signing and witness keys.
npx wrangler r2 bucket create static-ct-public-${LOG_NAME} [--location <location>]
npx wrangler kv namespace create static-ct-cache-${LOG_NAME}
openssl genpkey -algorithm ed25519 | npx wrangler -e=${ENV} secret put WITNESS_KEY_${LOG_NAME}
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 | npx wrangler -e=${ENV} secret put SIGNING_KEY_${LOG_NAME}Alternatively, simply run the script create-log.sh
(Note: For bootstrap_mtc_worker we use ed25519 for the signing key. There is no witness.)
- Deploy the worker. The worker will be available at
https://static-ct-${ENV}.<your-team>.workers.dev/logs/${LOG_NAME}.
npx wrangler -e=${ENV} deploy- Tail the worker:
npx wrangler -e=${ENV} tail- Send some requests. See local development for examples.
Follow these instructions to deploy to a custom domain, suitable for running a public CT log. We'll use the cftest environment as an example, which was used to deploy the [test logs][#test-logs].
-
Create a new deployment environment in
wrangler.jsoncby copying or editing the existingcftestenvironment. -
Create a file
config.${ENV}.jsonwith the configuration for the log shards. -
(Optional) Create a file
roots.${ENV}.pemwith any custom accepted roots for the log shards, in PEM format. Ifenable_ccadb_rootsis set to true, these roots are used in addition to roots auto-pulled from the CCADB list. Ifenable_ccadb_rootsis set to false for any logs in the deployment,roots.${ENV}.pemis required to exist and contain at least one certificate. All logs shards deployed within the same Worker script use the same set of additional roots. Roots can be updated later, but roots should generally not be removed once added. -
First set environment variables to specify the log shard name and deployment environment as below and then follow the instructions above to create resources for each log shard.
export LOG_NAME=cftest2025h1a export ENV=cftest -
Configure R2 buckets via Cloudflare dashboard. The monitoring APIs are served directly from the bucket, so configure for public access with caching and compression.
- Set up public access for the R2 bucket, either as a custom domain (recommended for caching) or as an r2.dev subdomain.
- Add a Cache Rule for the entire bucket, specifying
Respect origin TTLas theBrowser TTLoption. - Add a Compression Rule to enable compression for the
/tile/datapath.
-
Deploy the worker with
npx wrangler -e=${ENV} deploy.
-
Delete the worker via dashboard UI. This will delete all associated Durable Objects and secrets.
-
Delete associated KV namespaces via dashboard UI.
-
Delete associated R2 buckets via dashboard UI. You'll need to first delete all objects in the buckets, which you can either do manually (25 at a time, via the UI), or using a lifecycle management rule (recommended).
cargo doc --open --document-private-items
See the developer docs for guidance on
profiling and debugging. Use worker-build --dev as the build command in wrangler.toml to build with debug symbols.
cargo test
# to include tests that take several minutes to run, which are ignored by default
cargo test -- --ignored
cargo shear
This project ports code from sunlight and certificate-transparency-go.
The project is licensed under the BSD-3-Clause License.
location-hint: https://developers.cloudflare.com/r2/reference/data-location/ secrets-docs: https://developers.cloudflare.com/workers/configuration/secrets