Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Verifying the atc / atcd binary you’re running

atc and atcd ship as closed-source binaries. This doc explains the verification mechanism that already protects every install and self-update, and how you — a customer — can independently check that the binary on your machine or device is genuine and unmodified.

The digest check below (sha256 against the published release assets) needs only curl, sha256sum/shasum, and jq — no access to this repo. The signature check is stronger and needs nothing extra either: atc sign verify ships inside the atc binary you already installed. It has one honest limit: the check runs inside the very binary whose provenance you may be questioning. For a verifier fully independent of your installed atc, check the same signature with stock OpenSSL (recipe in that section). Either way you need no access to this repo. The same ed25519 check also already runs for you, automatically, on every self-update (see below).

If you are working in this codebase, see docs/development/README.md instead.

Mental model

Every release is described by a signed release index (index.json):

  • It lists, for each package (agency/atc, agency/atcd) and platform target (e.g. linux-x86_64-musl, darwin-arm64), the artifact’s sha256 digest, byte size, and download URL.
  • The index is signed with ed25519 by Agency’s release-signing key. The detached signature ships alongside it as index.json.sig.
  • The signing key is pinned inside the atc binary itself (crates/atc-update/src/keys.rs, AGENCY_PUBKEYS_HEX) — not fetched at runtime from anywhere an attacker on the network path could substitute.

So there are two independent checks, and both must pass before a binary is trusted:

  1. Digest check — the downloaded binary’s sha256 must equal the digest the index lists for that platform.
  2. Signature check — the index itself must carry a valid ed25519 signature from a key atc already has pinned in its binary.

A binary that matches the digest in a tampered (unsigned, or wrongly-signed) index does not pass. A signed index pointing at a digest that doesn’t match the downloaded bytes does not pass either. Both layers are required.

What verifies this automatically

You don’t have to do anything by hand for this to protect you — it already runs on every install and every self-update:

  • First install (curl -fsSL https://atc.dev/install.sh | sh, see tools/scripts/install.sh): fetches index.json over TLS, parses out the digest and URL for your platform, downloads the artifact, and refuses to install it if the computed sha256 does not match the digest in the index. This first hop relies on TLS + the digest for integrity — the install script itself does not check the ed25519 signature (it has no signing-key material and is deliberately dependency-free: no Python, no jq, just curl, awk, and sha256sum/ shasum).
  • Device install of atcd (curl -fsSL https://atc.dev/install-atcd.sh | sudo sh, see tools/scripts/install-atcd.sh): checks the ed25519 signature on index.json with openssl against the pinned key, before it reads the index. Then it checks that the index names the requested track, and the version if ATC_VERSION is set. Then it checks the sha256 of atcd against the signed digest. If any check fails, it stops and does not run atcd. It needs OpenSSL 3, for ed25519 with -rawin.
  • Every subsequent update (atc upgrade, implemented in crates/atc-cmd-bootstrap/src/self_update.rs and crates/atc-update/src/selfupdate.rs): the binary itself fetches index.json and index.json.sig, verifies the ed25519 signature against its pinned key set (crates/atc-update/src/sign.rs::verify_bytes, crates/atc-update/src/fetch.rs::fetch_verified_index), and only then downloads the artifact and checks its sha256 (fetch_artifact). If the source has no trusted key pinned, or the signature doesn’t verify, or the digest doesn’t match, the update is refused outright — nothing is ever applied on a verification failure. This is cryptographic, end-to-end authenticity, enforced by the binary itself, not by the transport.

In short: after your first install, you never have to trust a network path again — atc will not update itself to anything it can’t verify against its own pinned key.

Independently verifying an already-installed binary

If you want to check a binary yourself, rather than trusting that the updater did its job, here is exactly what to do. The digest check (step 2) needs sha256sum (or shasum), jq, and the published release assets that step 1 fetches with curl alone. The SHA256SUMS route at the end of step 2 needs no jq. The signature check (step 3) is one command: atc sign verify, built into the atc you already installed. The paranoid-grade alternative, for a verifier independent of that installed binary, is checking the same signature with stock OpenSSL.

The GitHub CLI (gh) is optional. You need it only to take the assets from a GitHub release instead of the origin, or to look up a release tag.

1. Get the published release assets

Every nightly build publishes, alongside the platform binaries:

  • SHA256SUMS — plain sha256sums of each staged binary
  • index.json — the signed release index described above
  • index.json.sig — its detached ed25519 signature

All three are attached to the corresponding GitHub release (e.g. nightly-2026-07-01). Fetch them either way — from the release origin, which needs nothing but curl:

track=nightly     # the track your atc came from: atc --version prints it
curl -fsSL "https://atc.dev/agency/$track/index.json"     -o index.json
curl -fsSL "https://atc.dev/agency/$track/index.json.sig" -o index.json.sig

The dev track is internal and token-gated, so those two curl commands return 403 for it. The edge serves /agency/dev/* only to requests that carry X-Atc-Channel-Token. Send the token to verify a dev build:

track=dev
curl -fsSL -H "X-Atc-Channel-Token: $ATC_CHANNEL_TOKEN" \
  "https://atc.dev/agency/$track/index.json"     -o index.json
curl -fsSL -H "X-Atc-Channel-Token: $ATC_CHANNEL_TOKEN" \
  "https://atc.dev/agency/$track/index.json.sig" -o index.json.sig

Get the token from 1Password, and export it as ATC_CHANNEL_TOKEN. The Opting in section of onboarding.md gives the item and the whitespace rule. The stable and nightly tracks are public, and they need no token. A dev build also has no GitHub release, so the gh release download alternative below cannot replace this fetch.

For a stable or nightly build you can also fetch from the GitHub release, which is where SHA256SUMS lives:

gh release download nightly-2026-07-01 -R Agency-Tool-Company/agency-code \
  -p 'index.json' -p 'index.json.sig' -p 'SHA256SUMS'

Finding the release tag for a binary you already have. atc --version prints the version and the track. Each track spells its tag differently, so do not guess the tag from the version alone:

atc --version   # e.g. "atc v0.1.6 (355c6f1013 2026-10-15T16:23:54Z, 0.1.6, track stable) [wire atc/6]"
                # or   "atc v0.1.7-nightly.20261016 (2b5eb47bb7 2026-10-16T12:50:16Z,
                #       0.1.7-rc.202610161245.2b5eb47bb7fa, track nightly) [wire atc/6]"

The leading token is a short human form. The exact version sits in the parenthetical, between the build date and the track, and that is the one the tag lookup below reads. For a script, read atc -V instead. It prints atc and the exact version, for example atc 0.1.6, so the version is the second field.

A build appends the wire generation it speaks, as a trailing [wire atc/N]. A release that predates the banner prints no such segment, and one that predates the short form leads with the exact version instead (see onboarding.md). The banner is not part of the version or the track, so it never changes the tag lookup below.

  • stable: the tag is v plus the version, so 0.1.3 becomes v0.1.3.

  • nightly: the tag is not derived from the version. A nightly version reads 0.1.4-rc.<YYYYMMDDHHMM>.<sha>, where the trailing <sha> is the commit it was built from. Match on that sha, not on the date. The tag is usually nightly-<YYYY-MM-DD> for the same day. A manually dispatched nightly can carry an operator-chosen release_date in its tag, while the version keeps the real build timestamp. The two dates then disagree. The sha always agrees:

    sha=2b5eb47bb7fa        # the trailing dot-field of the version in the parens
    gh api "repos/Agency-Tool-Company/agency-code/releases?per_page=100" \
      --paginate --jq ".[] | select(.name|contains(\"$sha\")) | .tag_name"
    # nightly-2026-10-16
    

    --paginate walks every release, so this also finds a nightly far older than any fixed gh release list -L <n> window would show. If you already know the date is trustworthy, gh release view nightly-<YYYY-MM-DD> is the quick path.

Both give you the same signed index. Step 2 below assumes index.json is in the current directory.

The atc.dev release origin the installer/self-update use serves index.json and index.json.sig, but not SHA256SUMS. The origin publishes binaries as content-addressed blobs keyed by the digests index.json already lists, so https://atc.dev/agency/<track>/SHA256SUMS returns 404 by design. So verify against index.json, which you always have; SHA256SUMS is a convenience if you happened to pull it from GitHub.

2. Check the digest

Check against index.json — it’s the one asset both the release origin and the GitHub release always carry, and it’s the file the signature in step 3 covers:

The index stores the digest as sha256:<hex>, while sha256sum/shasum print bare hex, so strip the prefix before comparing rather than eyeballing the two:

bin=atc-linux-x86_64-musl                     # the artifact you downloaded
# ...or the binary already on this machine:
# bin=$HOME/.local/bin/atc
target=linux-x86_64-musl                      # its key in index.json

got=$(sha256sum "$bin" | cut -d' ' -f1)       # Linux
# macOS: got=$(shasum -a 256 "$bin" | cut -d' ' -f1)

want=$(jq -r --arg t "$target" \
  '.artifacts."agency/atc"[$t].digest' index.json | sed 's/^sha256://')

[ "$got" = "$want" ] && echo "digest OK" || echo "MISMATCH: $got != $want"

If you pulled SHA256SUMS from the GitHub release, it checks every binary in one command (but see above — the origin doesn’t serve it):

sha256sum -c SHA256SUMS      # Linux
shasum -a 256 -c SHA256SUMS  # macOS

If your locally-installed atc/atcd was placed by the installer or by atc upgrade, this is the same check those tools already performed for you — a mismatch here would mean something altered the binary after it was verified and installed (e.g. local tampering on disk), since both paths refuse to write a binary whose digest doesn’t match in the first place.

3. Check the index signature

The signature check is what stops a compromised or spoofed download origin from ever making it to your machine — even if an attacker could rewrite index.json and its listed digests to point at a malicious binary, they cannot forge a valid signature over it without the private key (held in 1Password, surfaced to CI only as ATC_RELEASE_SIGNING_KEY; see crates/atc-update/src/keys.rs).

The key below can rotate. Don’t treat it as permanent. atc supports signing-key rotation: a future release can add a new key alongside this one for a transition window, then retire this one. The canonical, always-current source of truth for which key(s) are trusted is AGENCY_PUBKEYS_HEX in crates/atc-update/src/keys.rs at the commit your installed atc was built from — not this doc. If a signature check against the key below ever fails on an otherwise-current release, check that file (or ask Agency) before assuming tampering; it may mean the key has rotated since this page was last updated.

The production public key, pinned in every atc build as of this writing, is:

8f0661861de15f450781f8cdb08a33e094aff7c67539371d9ecc4dd12362202d

(from AGENCY_PUBKEYS_HEX, which is a set — it can carry more than one key during a rotation window; this is the sole key in production today.) The first 8 bytes of that key, hex-encoded, are the key_id prefix on every index.json.sig line — you can confirm at a glance that a given signature claims to be from this key before doing the cryptographic check:

$ cat index.json.sig
8f0661861de15f45 28436bd3a8afc3686edf45e9af39862b41bbe514692f55f41260677e8aec3b3624907251769d651102235db03cb9811905e904d4853eb2289587378829bb4a07
# ^^^^^^^^^^^^^^^^ key_id — matches the first 8 bytes of the pinned pubkey above

To actually verify the ed25519 signature (not just eyeball the key id), run atc sign verify — the exact verification code path atc’s own self-update uses (atc_update::sign::verify_bytes), exposed as a subcommand for exactly this purpose:

atc sign verify index.json index.json.sig \
  8f0661861de15f450781f8cdb08a33e094aff7c67539371d9ecc4dd12362202d
# -> OK   (signature verifies against the pinned key; exit 0)
# -> VERIFY FAILED: ...   (reject the index — do not trust any binary it points at; exit 1)

Pass more than one pubkey during a rotation window; the check passes if the signature matches any of them. atc sign also carries keygen and create for producing your own detached signatures.

One honest caveat: this check runs inside the very atc binary whose provenance you may be questioning, and a hypothetically malicious atc could lie about it. If that’s your threat model, verify with a tool that isn’t ours. The format is deliberately standard, so stock OpenSSL 3.x does it. The one wrinkle is that index.json.sig is a line of two hex fields, <key_id> <signature> (the key_id selects a key during rotation), so pass field 2 rather than the whole file:

# Field 2 of the .sig line is the raw 64-byte ed25519 signature.
awk '{print $2}' index.json.sig | xxd -r -p > sig.raw

# Wrap the pinned hex public key in the standard ed25519 DER prefix.
printf '302a300506032b6570032100%s' <pinned-pubkey-hex> | xxd -r -p > atc-pub.der

openssl pkeyutl -verify -pubin -inkey atc-pub.der -keyform DER \
  -rawin -in index.json -sigfile sig.raw
# -> "Signature Verified Successfully"

Putting it together

A binary is genuine and untampered iff both hold:

  1. sha256(binary) == index.json's digest for that platform/package, and
  2. index.json’s signature verifies against a key in the trusted set (8f0661861de15f450781f8cdb08a33e094aff7c67539371d9ecc4dd12362202d as of this writing — see the rotation caveat above).

If either check fails, do not run the binary — and note that atc’s own installer and self-update already refuse to complete in that situation, so a failure here on a binary that came from those paths would itself be a strong signal of something wrong outside atc’s control (a compromised local disk, a mirrored/proxied download that served different bytes, etc.).

Where the trust root comes from

  • The ed25519 keypair was generated via a secure host + 1Password (op) ceremony (see crates/atc-update/src/keys.rs for the pointer to the private-key location: op://Agency/atc-release-signing/private-key).
  • The private half never leaves 1Password/CI: it’s surfaced to the nightly release workflow only as the ATC_RELEASE_SIGNING_KEY GitHub Actions secret, used solely to sign index.json at release time (tools/scripts/stage-nightly-release-binaries.sh).
  • The public half is compiled into the atc binary as a Rust constant (AGENCY_PUBKEYS_HEX) — there is no runtime fetch of “the current trust key” that a network attacker could redirect. Trusting a given atc binary build (via the digest/signature chain above, or by building from this source yourself) is what transitively trusts the key it carries.
  • AGENCY_PUBKEYS_HEX is a set, not a single key, specifically so the signing key can be rotated: a future build adds the new key alongside the old one for a rotation window, then drops the retired one once every supported atc has upgraded past it.

What this does not cover

  • The signature check has a CLI verb (atc sign verify); the digest check does not. There is no atc doctor that runs both against your installed binary and reports one verdict. Do the digest check by hand, as described above.
  • There is no standalone public page publishing the current trust key. Both the digest and signature checks are fully self-service with standard tools (sha256sum, OpenSSL), but the trusted key itself reaches you either baked into an atc build or copied from this page. Treat the pinned key printed above as a snapshot, not a permanent value, and see the rotation caveat in the signature-check section.
  • This verifies what Agency’s release pipeline published, not that the source code matches some external audit — it’s a supply-chain integrity check (this exact binary came from this exact signed release), not a reproducible-build proof.