Witness operator guide

Run a witness

The whole flow, start to finish: run standard open-source witness software on your own infrastructure, host a small HTTPS endpoint, apply, and cosign mosskeys checkpoints as an independent party. No mosskeys account, token, or CLI involved — the C2SP tlog-witness protocol is the only contract.

The flow at a glance

  1. Run the software. A small binary (omniwitness or equivalent) on a VPS, a Pi, or spare capacity.
  2. Mint keys, host the endpoint. An Ed25519 cosignature keypair (optionally ML-DSA-44 too) and an HTTPS submission prefix on a domain you control.
  3. Apply. The application takes your operator identity, endpoint, and vkeys — checked cryptographically as you type.
  4. Human review. We verify independence (operator, infrastructure, jurisdiction) and probe that your endpoint answers (a 404 to an unknown origin is the healthy pre-activation response).
  5. Go live. Poll the log-discovery feed for the origins and vkeys to follow (approval emails you a pointer automatically), we activate the relay, and your listing appears on the public directory.

Ready to get started? Head on over to the become a witness page, or keep reading to learn more.

Get the software

mosskeys speaks the stock witness protocol, so any correct implementation works — the ecosystem's software or ours. Whichever you run, run it on infrastructure you control: a witness's value is the independence of its operator, and listing software here is not our endorsement of any particular deployment.

Software What it is
omniwitness The reference Go witness from the transparency-dev team. Single binary or Docker Compose, sqlite state, optional bastion mode if you'd rather not expose the box directly. The default path for most operators.
armored-witness The same witness code as bare-metal firmware for a dedicated USB armory device, with reproducible builds logged in a firmware transparency log. Maximum operator independence, minimal maintenance.
sigsum The sibling transparency ecosystem whose witness network runs the same protocol. Already witnessing sigsum? Adding mosskeys is a config change, not new software.
mosskeys-witness Our first-party Rust witness, built on the metamorphic-log crate. It answers with two cosignatures by default — the spec-standard Ed25519 (0x04) cosignature every ecosystem verifier understands, plus the spec-assigned post-quantum ML-DSA-44 (0x06) cosignature — minted as two independent keypairs, so one key compromise never implies the other. Speaks the same C2SP protocol with an omniwitness-compatible config, so it can run alongside your existing witness or by itself.

Why our own first-party witness implementation?

Transparency logs stay honest because independent witnesses cosign checkpoints. If a log ever forks and shows different clients different history, those cosignatures are the evidence that proves it. The ecosystem works, but it is small: the widely deployed witnesses (omniwitness, armored-witness, sigsum) still sign Ed25519-only in practice.

The tlog-cosignature spec standardized the timestamped Ed25519 cosignature (0x04); the merged ML-DSA-44 cosignature update assigns 0x06 — timestamped ML-DSA-44 (sub)tree cosignatures — and recommends ML-DSA-44 cosignatures for new witness deployments. mosskeys dual-signs every checkpoint at the origin — a strict-AND Ed25519 + ML-DSA hybrid line (ML-DSA-65 at Category 3, ML-DSA-87 at Category 5) plus a classical Ed25519 (0x01) line that stock Ed25519-only witness software verifies unmodified — and the metamorphic-log engine implements that assigned 0x06 format exactly: the subtree/v1 signed message, the key-id formula, the vkey encoding. The mosskeys registry accepts both cosignature types.

mosskeys-witness is the post-quantum-native way to run one. It is a Rust witness built on the metamorphic-log crate, and it answers every request with two cosignatures — Ed25519 for interop with today's tooling, ML-DSA-44 for post-quantum security — minted as two independent keypairs and produced on every checkpoint by default, not opted into. It speaks the same C2SP protocol as omniwitness and keeps configuration compatible, so you can run it alongside your existing witness or by itself. We built it to grow the ecosystem, not consolidate it: mosskeys deployments never count witnesses they operate toward a log's witnessed quorum.

install mosskeys-witness
# One-line installer (macOS + Linux, incl. musl — verifies SHA-512 before installing)
curl -fsSL https://mosskeys.com/install.sh | sh -s -- witness

# Homebrew (macOS + Linux)
brew install moss-piglet/mosskeys-witness/mosskeys-witness

# crates.io
cargo install mosskeys-witness --locked

# Docker (GHCR — multi-arch linux/amd64+arm64, FROM scratch, runs non-root)
docker pull ghcr.io/moss-piglet/mosskeys-witness:0.2.0

# Verify the image's keyless cosign signature before first run:
cosign verify ghcr.io/moss-piglet/mosskeys-witness:0.2.0 \
  --certificate-identity-regexp 'https://github.com/moss-piglet/mosskeys-witness/\.github/workflows/release\.yml@refs/tags/v.*' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com

# Signed release tarballs (verify SHA512SUMS + cosign bundle before running):
# https://github.com/moss-piglet/mosskeys-witness/releases

Any implementation that speaks tlog-witness and returns tlog-cosignature v1 signatures works, including one you write yourself. The quickstart below uses our container image because it is the fastest path; the sections after it are software-agnostic and use omniwitness where a concrete command helps.

Quickstart: mosskeys-witness in five minutes

Three steps: mint your keys, write one config file, run the signed container image. Nothing here needs a mosskeys account, and everything except the final curl works offline.

1. Mint your keys

Pick a cosigner name: a schema-less URL under a domain you control, such as witness.example.com/mosskeys. It is baked into every cosignature and into both vkeys, so choose one you can keep. This is the one step that runs on your machine — any install option from Get the software above works. Everything after it runs in Docker.

keygen
# One directory holds everything — witness.toml, keys/, state/. Create it
# and stay in it for the whole quickstart: the container bind mounts below
# are relative to your current directory.
mkdir -p ~/mosskeys-witness && cd ~/mosskeys-witness

# Mint the witness identity offline: two independently generated keypairs,
# Ed25519 (0x04) and ML-DSA-44 (0x06). Seeds are written 0600 and never
# overwritten; only the public vkeys are printed. keygen is the only step
# that needs the binary — any install option above works; the witness
# itself runs in Docker.
mosskeys-witness keygen --name witness.example.com/mosskeys --out-dir ./keys

# Witness identity minted for witness.example.com/mosskeys
#
# Ed25519 vkey (public — register with logs):
#   witness.example.com/mosskeys+1a2b3c4d+BC...
#   seed file (secret, mode 0600): ./keys/ed25519.seed
#
# ML-DSA-44 vkey (public — register with logs):
#   witness.example.com/mosskeys+5e6f7a8b+Bg...
#   seed file (secret, mode 0600): ./keys/mldsa44.seed

# Keep both vkeys: they are what you paste into the application.

2. Write the config

A witness only cosigns logs it has been told about, so the config carries the (origin, vkeys) pair for each one. Those pairs come from our public discovery feed. Run this once, anywhere — your laptop is fine. It does not go in the container image, and there is no cron to set up yet: the response is simply what you copy into the config file below.

shell
curl https://mosskeys.com/api/witness/logs
the feed's answer
{
  "logs": [
    {
      "origin": "mosskeys.com/acme",
      "vkeys": {
        "hybrid": "mosskeys.com/acme+<key id>+<base64 hybrid vkey>",
        "ed25519": "mosskeys.com/acme+<key id>+<base64 Ed25519 0x01 vkey>"
      }
    }
  ]
}

Each entry in logs becomes one [[log]] stanza: the origin verbatim, and both vkey strings as the vkeys list. Copy them by hand, or generate the stanzas straight from the feed:

generate the [[log]] stanzas
curl -s https://mosskeys.com/api/witness/logs | jq -r \
  '.logs[] | "[[log]]\norigin = \"\(.origin)\"\nvkeys = [\n  \"\(.vkeys.hybrid)\",\n  \"\(.vkeys.ed25519)\",\n]\n"'

Save the following as witness.toml in the directory you created in step 1, next to keys/, replacing the placeholder [[log]] stanza with the real one(s) from above. The paths are the ones inside the container.

witness.toml
name = "witness.example.com/mosskeys"
listen = "0.0.0.0:8080"
state_file = "/state/state.jsonl"

[keys]
ed25519_seed = "/keys/ed25519.seed"
mldsa44_seed = "/keys/mldsa44.seed"

# One stanza per log you cosign, copied from the discovery feed. Origins
# are exact: no wildcards, no prefixes. Anything else answers 404.
[[log]]
origin = "mosskeys.com/acme"
vkeys = [
  "mosskeys.com/acme+<key id>+<base64 hybrid vkey>",
  "mosskeys.com/acme+<key id>+<base64 Ed25519 0x01 vkey>",
]

3. Run it

run the container
# Multi-arch, FROM scratch, no shell, runs as uid 65532. Verify the image's
# keyless signature before the first run.
cosign verify ghcr.io/moss-piglet/mosskeys-witness:latest \
  --certificate-identity-regexp 'https://github.com/moss-piglet/mosskeys-witness/\.github/workflows/release\.yml@refs/tags/v.*' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com

# keys/ must be readable and state/ writable by that uid (or pass
# --user "$(id -u):$(id -g)" to run as your host uid instead). The seeds
# stay 0600: only the owner changes, so they remain owner-only.
mkdir -p state && sudo chown -R 65532:65532 keys state

# Run from the directory you created in step 1: the bind mounts are
# relative to your current directory, and Docker silently creates a
# missing host path as an empty directory — the witness then fails with
# "Is a directory (os error 21)".
docker run -d --name mosskeys-witness -p 8080:8080 --restart unless-stopped \
  -v ./witness.toml:/witness.toml:ro -v ./keys:/keys:ro -v ./state:/state \
  ghcr.io/moss-piglet/mosskeys-witness:latest run --config /witness.toml

# Startup is fail-closed: seed permissions, vkeys matching the configured
# name, duplicate origins, and the state lock are all checked before the
# listener opens. Both cosigner vkeys are reprinted in the banner.
docker logs mosskeys-witness

# Healthy pre-activation answer to an origin you do not cosign: 404 — the
# request parsed and only the allowlist refused it (an empty body is a 400,
# which proves nothing). This is the check our review probe makes. The body
# is the tlog-witness wire format: "old 0", a blank line (no consistency
# proof), the checkpoint (origin, tree size, root hash), then its signature
# line — which really does start with an em-dash, "— <name> <base64>". The
# short placeholder sig is never checked: the origin refusal is evaluated
# before any signature is even looked at.
curl -si -X POST --data-binary @- http://localhost:8080/add-checkpoint <<'EOF' | head -1
old 0

unknown.example/log
1
AARpcm88QZxj7jR9izc5sCygNRvIk0Ym2MCPmtKGxBk=

— unknown.example/log AAAAAAAA
EOF

Now put HTTPS in front of that port on a domain you control. Your submission prefix is whatever public URL reaches it, for example https://witness.example.com/mosskeys; we POST to <prefix>/add-checkpoint. Any reverse proxy will do, and if you would rather not expose the box at all, use a bastion.

4. Apply

Fill in the application with who operates the witness, a contact email, the cosigner name, the submission prefix, and both vkeys from step 1. The form checks the vkeys cryptographically as you type, then a human reviews independence. Expect a few days.

5. Then it just runs

On approval we add your vkeys to the submission registry and checkpoints begin arriving on their own. Each one is a single POST carrying the signed note, the size you last cosigned, and the consistency proof between them; your witness verifies the proof, appends to its state file, and answers with two cosignatures. Your listing appears on the public directory with a live count of the cosignatures we have merged from your key.

Day to day there is nothing to do but keep it reachable. New logs reach you two ways, so you never have to remember either: the discovery feed is ETag-conditional, so a cron that sends If-None-Match gets a cheap 304 until something actually changes; and we also email you whenever new origins start being relayed. Downtime is safe: submissions retry with backoff and resume from your last cosigned size. If submissions keep failing we pause them rather than hammer you, and you get an email saying so — your listing goes back to live the moment it is fixed. Updates are a docker pull and a restart, and your keys and state survive it.

Mint your keys

Whatever software you run, a witness is identified on the wire by a cosigner name, a schema-less URL under a domain you control like witness.example.com/mosskeys, plus one or two cosignature keypairs. With mosskeys-witness that is keygen above; with omniwitness it is its key tool:

mint cosignature keys
# Ed25519 cosignature keypair (0x04) — what stock witnesses sign with today.
go run github.com/transparency-dev/witness/cmd/generate_keys@main \
  --origin "witness.example.com/mosskeys" \
  --out_priv ./witness.sec --out_pub ./witness.pub

# Optional: ML-DSA-44 cosignature keypair (0x06) — the type the
# tlog-witness spec recommends for new deployments.
go run github.com/transparency-dev/witness/cmd/generate_keys@main \
  --origin "witness.example.com/mosskeys" --mldsa \
  --out_priv ./witness-mldsa.sec --out_pub ./witness-mldsa.pub

# .pub is the vkey you paste into the application. .sec never leaves the
# box (the tool writes it 0600 and refuses to overwrite).
  • Ed25519 (0x04) is what witness software signs with today and what every ecosystem verifier understands. Bring this one at minimum.
  • ML-DSA-44 (0x06) is the assigned post-quantum cosignature type — timestamped ML-DSA-44 (sub)tree cosignatures — and the one the spec recommends for new deployments. Optional; our registry accepts it either way.

The public .pub lines are the verifier keys (vkeys) you paste into the application:

vkey anatomy
<name>+<8-hex key id>+<base64(type byte || public key)>

key id = first 4 bytes of SHA-256(name || 0x0A || type || public key), hex
type   = 0x04 (Ed25519, 32-byte key) or 0x06 (ML-DSA-44, 1312-byte key)

Because the key id commits to the name, the type, and the key, the application form can check your paste on the spot. A vkey whose embedded name does not match your cosigner name, or whose key id does not re-derive, is rejected before any human sees it.

Host the endpoint

Run the witness, then put HTTPS in front of it. Your submission prefix is the public HTTPS URL we POST add-checkpoint to.

run omniwitness
# Single Go binary, sqlite for per-log state. Docker Compose works too —
# see the omniwitness README.
go run github.com/transparency-dev/witness/cmd/omniwitness@main \
  --alsologtostderr \
  --private_key_path ./witness.sec \
  --db_file ./witness.db

# Put HTTPS in front (a domain you control) and your submission prefix is
# e.g. https://witness.example.com/tlog — mosskeys POSTs to
# <prefix>/add-checkpoint.
  • HTTPS on a domain you control. A cosignature served over a channel someone can intercept is worse than none, so the application requires https. Plain http is accepted only for loopback hosts, for dev.
  • Tiny footprint. A small binary and one state file. A cheap VPS, a Raspberry Pi, or spare capacity you already have. Pennies a month.
  • Mostly online beats perfectly online. If your endpoint is down, submissions retry with backoff and resume from the last tree size you cosigned. Gaps never break consistency.
  • No public exposure? Use a bastion. The https-bastion protocol lets your witness stay behind a proxy, and omniwitness supports it natively. Give us the bastion URL including your backend key hash, so https://<bastion>/<sha256 of your backend key, lowercase hex>, since the bastion strips that segment before proxying. Hand us the bastion root instead and our review probe gets a 421 (unknown backend) or 503 (backend not connected) rather than the healthy 404.

Hosting on Fly.io? The Fly.io deployment guide walks one Machine, one volume, and your own domain, step by step.

Follow the mosskeys logs

Each mosskeys namespace is its own transparency log with its own origin, mosskeys.com/<slug> (for example mosskeys.com/acme), signed by that namespace owner's own key. Your witness only cosigns origins it is configured to trust and answers 404 for everything else, so before the first checkpoint arrives you need the origin and vkey of each namespace you witness.

We publish that as a public, unauthenticated feed. GET https://mosskeys.com/api/witness/logs returns exactly the relayed set, every namespace whose checkpoints we submit to witnesses, as {origin, vkeys} pairs, and it updates as namespaces come online. Poll it to keep your config current. It is per-IP rate limited and cacheable for 60 seconds.

shell
curl https://mosskeys.com/api/witness/logs
200 OK
{
  "logs": [
    {
      "origin": "mosskeys.com/acme",
      "vkeys": {
        "hybrid": "mosskeys.com/acme+<key id>+<base64 hybrid vkey>",
        "ed25519": "mosskeys.com/acme+<key id>+<base64 Ed25519 0x01 vkey>"
      }
    }
  ]
}

The response carries a strong ETag over the exact bytes, so polling costs almost nothing: send it back as If-None-Match and you get 304 Not Modified with an empty body until the relayed set actually changes. The tag is a hash of the feed contents, not a timestamp, so it is stable across our restarts and deploys — a five-minute cron is entirely reasonable.

conditional poll
# First poll: save the ETag alongside the feed.
curl -sS -D headers.txt -o logs.json https://mosskeys.com/api/witness/logs
grep -i '^etag:' headers.txt | cut -d' ' -f2- | tr -d '\r' > logs.etag

# Every poll after: 304 with an empty body when nothing changed, so this
# is cheap enough to run every few minutes.
curl -sS -o logs.new.json -w '%{http_code}\n' \
  -H "If-None-Match: $(cat logs.etag)" \
  https://mosskeys.com/api/witness/logs
# 304  -> nothing to do
# 200  -> new logs; reload your witness config, then save the new ETag

We push as well as serve: whenever new origins start being relayed, every live witness operator gets an email listing them. The feed stays the source of truth and the email is the backstop — an email can bounce, the feed cannot go stale.

How you act on a change is up to you: hand-paste the stanzas, cron a one-shot sync, or let the witness follow the feed itself. Keep your origin set current walks the three postures below.

The hybrid vkey is the post-quantum key our own verifiers trust. The ed25519 vkey is the classical 0x01 key derived from the same namespace key, which Ed25519-only witness software verifies against. Configure both if your software accepts both. On approval we email you the current set to get you started.

What then lands at your endpoint is one HTTP call per published checkpoint: the signed note, the size you last cosigned, and the RFC 6962 consistency proof between them. The become a witness page shows the full wire exchange.

Apply

With the endpoint live and the vkeys in hand, fill in the application. It asks who operates the witness (published on the directory), a private contact email, the cosigner name, the submission prefix, and at least one vkey.

  • Checked by software: vkey structure, key-id re-derivation, name match, https prefix, and a deliverable contact email, all at the form, before review.
  • Checked by a human: your organization, infrastructure, and jurisdiction, to judge how much independent value the witness adds. Expect a few days.

Applying is an offer to operate independently. It is not a mosskeys account, a contract, or a guarantee of listing, and we never run a witness on an applicant's behalf.

Review and going live

If approved, we add your witness to the deployment's submission registry under a key we bind at review time, and every Pro-tier checkpoint starts arriving at your endpoint automatically. You are listed on the public directory (and its JSON twin, GET https://mosskeys.com/api/witnesses) with the operator identity you supplied.

The directory shows your listing as activating until the registry entry is live, then live with a count of the cosignatures actually merged from your key. Every cosignature you serve is also visible on the checkpoints themselves.

Listing is transparency, not our endorsement. Verifiers pin whichever witness vkeys they trust and check cosignatures themselves.

Operate

Day to day, four rules, and your software enforces the first one for you:

  • Never cosign two conflicting checkpoints at the same tree size. Remember the last checkpoint you cosigned per log, atomically, and refuse anything that does not extend it. That refusal is the entire security property, which is also why you must never run two live instances off a copied witness database.
  • Downtime is fine. Submissions retry with backoff, and when you come back the relay re-proves from your last cosigned size and carries on. A size disagreement resolves per protocol, via the 409 re-proof exchange.
  • Stay reachable. If your endpoint, keys, or operator details change, write to the review thread from your application email. Stale witness configs are retired, not silently left in place.
  • Rotate deliberately. Key rotation follows your software's procedure, and omniwitness signs with the old and new key during overlap. Tell us before you switch so the directory and registry stay accurate.

Keep your origin set current

Namespaces come online over time, and your [[log]] stanzas are the policy that decides what you cosign, so they need to track the discovery feed. There are three postures for that. All three read the same feed, so you can start with one and switch later without changing anything else. Pick whichever matches how you like to run things.

1. Manual: the floor

When the new-origin email lands (or your own poll turns up a change), regenerate the stanzas from the feed, paste them over the old ones in witness.toml, and restart or redeploy. Two minutes, works with every witness implementation, and every change passes through your eyes and your git history. The generator from the quickstart:

regenerate the [[log]] stanzas
curl -s https://mosskeys.com/api/witness/logs | jq -r \
  '.logs[] | "[[log]]\norigin = \"\(.origin)\"\nvkeys = [\n  \"\(.vkeys.hybrid)\",\n  \"\(.vkeys.ed25519)\",\n]\n"'

This is the floor the other two postures automate, and it stays the fallback forever: the feed plus your editor is always enough. It fits operators who want to review every change by hand, and anyone running non-mosskeys witness software.

2. One-shot sync on a schedule

mosskeys-witness sync (0.3.0 and later) fetches the feed itself, validates every entry with the same fail-closed rules as config load, and writes a managed file, discovered_logs.toml, next to your state file, atomically. The witness loads that file at startup and merges it with your manual stanzas; a manual stanza always wins over a managed entry for the same origin, so you can pin specific logs by hand and let the feed handle the rest. Syncing is a separate code path: the serving witness never dials out.

Run it from cron, certbot-style. The exit code says what happened (0 unchanged, 10 updated, 1 error), so the restart fires only when the managed file actually changed:

one-shot sync, on a schedule
# Refresh the managed allowlist once, by hand. It lands as
# discovered_logs.toml next to your state file and the witness loads it at
# startup, so a change takes effect on the next restart. Exit codes:
# 0 unchanged, 10 updated, 1 error.
mosskeys-witness sync --config /etc/mosskeys-witness/witness.toml

# /etc/cron.d/mosskeys-witness — every 15 minutes, restart only on change:
*/15 * * * * root mosskeys-witness sync --quiet --config /etc/mosskeys-witness/witness.toml; [ $? -eq 10 ] && systemctl restart mosskeys-witness

# Running the container instead? Same contract, through a throwaway
# container sharing the state mount:
*/15 * * * * root docker run --rm -v /etc/mosskeys-witness/witness.toml:/witness.toml:ro -v /var/lib/mosskeys-witness:/state ghcr.io/moss-piglet/mosskeys-witness sync --quiet --config /witness.toml; [ $? -eq 10 ] && docker restart mosskeys-witness

Prefer systemd timers to cron? Two units ship in packaging/systemd in the mosskeys-witness repo: a oneshot service that runs sync and restarts the witness only on exit 10, and a timer that fires it every fifteen minutes. The restart itself is safe: submissions retry with backoff and resume from your last cosigned size. This posture fits operators who want updates applied automatically but keep restart-based config management, on a box they already run under systemd or Docker.

3. In-process auto-sync: the recommended default

The zero-maintenance posture. Add one [discovery] section to witness.toml (0.4.0 and later) and the witness follows the feed by itself: the first poll runs at boot without blocking startup, then every interval, and a change hot-swaps the in-memory allowlist with no restart at all. New origins start getting cosignatures within one interval. A failed poll is logged and non-fatal, and the managed file on disk carries the last known set across restarts.

witness.toml
# Add once, then redeploy one last time. From here the witness polls the
# feed itself: first at boot, then every interval, and a change hot-swaps
# the in-memory allowlist. New origins start getting cosignatures within
# one interval, no restart and no cron. A failed poll is logged and
# non-fatal: the last known set keeps serving.
[discovery]
feed_url = "https://mosskeys.com/api/witness/logs"
interval_secs = 300   # optional; this is the default

Flip the switch once and the origin set stays current forever. This fits almost everyone, which is why it is the default we recommend. The trade to know about: the feed URL you pin is the vetting boundary for managed entries, so a vkey rotation served by the feed is applied at the next poll. That is the same trust you already place in the feed for the origin list itself. To vet one log's rotations by hand, keep a manual [[log]] stanza for that origin: manual always wins.

Three rules, whichever posture you pick

  • Follow the whole feed. A partial config answers 404 for every origin you omit, and those checkpoints keep arriving and keep being refused, with nothing to tell you an origin is missing beyond the refused requests. The feed is exactly the relayed set, so following all of it is the one configuration that never surprises.
  • Removals stop new cosignatures, never delete state. If an origin leaves the feed it drops out of the managed file and you stop cosigning its new checkpoints, but your per-log state is never touched: past cosignatures stay valid, and if the origin returns you resume from your last cosigned size.
  • Rotations arrive by re-sync. When a namespace rotates its key, the feed serves the new vkey and the next sync, poll, or paste applies it. Pin the origin with a manual stanza if you want a rotation to wait for your review.

Who trusts whom

Three parties touch every cosigned checkpoint, and each one makes its own trust decision. Nobody's choice obligates anyone else:

  1. Verifiers pin the witness vkeys they trust. A relying party (an app, a library, a fleet) decides for itself which witnesses it counts, pins those vkeys, and checks cosignatures locally. The directory is a discovery input, never a root of trust.
  2. The registry decides who gets relayed to. mosskeys reviews applications, submits checkpoints to approved witnesses, and merges only their cosignatures into the served note. That is an operational decision, not a security gate.
  3. A witness declares which origins it cosigns. The [[log]] stanzas in your config are your own policy. Adding an origin never obligates the log or the namespace owner: they did not ask for your cosignature, and they are free to ignore it.

Two consequences follow. First, the served note only ever carries cosignatures from approved registry witnesses, so the witness lines on a note come pre-screened for liveness and independence. Second, the logged party does not pick its own witnesses. That is deliberate: a namespace choosing who watches it would undermine the independence property, the way a company choosing its own auditors would. The registry exists for operational quality (small notes, live witnesses, vetted independence), not because an unwanted cosignature is a security threat. A cosignature a verifier does not trust is just a line it ignores.

The verifier's side of this, including how to pin vkeys and how to treat cosignatures in code, is on the read page under verifier integration best practices.

See also

  • Run a witness on Fly.io: the hand-held deployment guide, from keygen to a live witness on one Machine and one volume.
  • Apply to witness: self-serve application, cryptographically self-validating vkeys, and human review for operator independence.
  • Become a witness: what a witness proves, why independence is the point, and the add-checkpoint wire exchange.
  • Public witness directory: the live roster, jurisdictions, vkeys to pin, and observed cosignature counts.
  • mosskeys-witness: our first-party dual-signing witness, with the container image, signed releases, threat model, and conformance checklist.
  • C2SP tlog-witness and tlog-cosignature: the protocol and signature format, for implementers.
  • Read & verify API: the public endpoints your users (and you) verify checkpoints against.