Documentation Index
Fetch the complete documentation index at: https://agenticadvertisingorg-changeset-release-main.mintlify.app/llms.txt
Use this file to discover all available pages before exploring further.
Every AdCP release publishes a {version}.tgz bundle (the full schema + compliance + OpenAPI tree at that version) along with three sidecars:
| File | Role |
|---|
{version}.tgz.sha256 | SHA-256 checksum, in-transit integrity |
{version}.tgz.sig | Sigstore detached signature |
{version}.tgz.crt | Fulcio-issued signing certificate |
The SHA-256 sidecar lives on the same origin as the tarball, so it only protects against transit tampering. The .sig + .crt pair proves the bundle came from the AdCP release workflow itself and was not swapped for a malicious one even if the host were compromised.
This page covers how to verify those signatures correctly. SDK users (@adcp/sdk, adcp-client-python, adcp-go) get this verification for free on every sync-schemas / download.sh run. If you’re consuming the bundle directly — pinning a specific version in a CI pipeline, ingesting it from a different language, or implementing a fresh adopter — read on.
Trust model
AdCP uses Sigstore keyless signing. There is no long-lived private key. At release time:
- The
release.yml workflow on adcontextprotocol/adcp runs on a GitHub Actions runner.
- The runner mints a short-lived OIDC token whose subject identifies the workflow and ref that produced the run.
cosign sign-blob --yes exchanges that OIDC token at Sigstore’s Fulcio CA for a short-lived X.509 certificate, then produces a detached signature using the cert’s ephemeral private key.
- The signature, certificate, and a transparency log entry land in Sigstore’s Rekor public log.
- The release pipeline commits
.sig and .crt next to the tarball and uploads them to the GitHub Release.
Verification on the consumer side then checks two binding properties:
- Signature authenticity — the
.sig was produced by the private key that the .crt certifies. Standard Sigstore math; no AdCP-specific.
- Identity binding — the
.crt’s subject names the AdCP release workflow specifically, with the issuer being GitHub Actions’s OIDC provider. This is the AdCP-specific part.
If both hold, you have proof that an AdCP release workflow run produced this exact tarball — provable end-to-end without trusting adcontextprotocol.org itself.
Recommended cosign verify-blob invocation
# Download the tarball + sidecars
curl -OL https://adcontextprotocol.org/protocol/3.0.3.tgz
curl -OL https://adcontextprotocol.org/protocol/3.0.3.tgz.sha256
curl -OL https://adcontextprotocol.org/protocol/3.0.3.tgz.sig
curl -OL https://adcontextprotocol.org/protocol/3.0.3.tgz.crt
# Verify checksum first (cheap, catches in-transit corruption)
shasum -a 256 -c 3.0.3.tgz.sha256
# Verify Sigstore identity (proves publisher)
cosign verify-blob \
--signature 3.0.3.tgz.sig \
--certificate 3.0.3.tgz.crt \
--certificate-identity-regexp '^https://github\.com/adcontextprotocol/adcp/\.github/workflows/release\.yml@refs/(heads|tags)/.*$' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
3.0.3.tgz
Both must exit zero before extracting. cosign verify-blob returns non-zero if the signature was made by anything other than the AdCP release workflow, even if the SHA matches and TLS is valid.
The identity regex, explained
^https://github\.com/adcontextprotocol/adcp/\.github/workflows/release\.yml@refs/(heads|tags)/.*$
Three pieces matter:
https://github.com/adcontextprotocol/adcp/.github/workflows/release.yml — the workflow file path. This is what makes the certificate AdCP-specific. A workflow in a different repo, or a different workflow file in this repo, won’t match.
refs/(heads|tags)/.* — the ref the workflow ran against. Branch refs are what’s used today (cosign signs during the push-triggered run, so the OIDC subject is release.yml@refs/heads/<branch>). Tag refs are forward-compat for any future post-tag re-signing flow.
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' — the OIDC issuer must be GitHub Actions itself. Even with the right repo and workflow path, a non-GitHub-Actions issuer would fail this check.
Why a regex, not an exact ref
The first version of this regex was ^...refs/heads/(main|2\.6\.x)$ — a literal allowlist of release branches. It silently rejected v3.0.1+ when those releases moved to refs/heads/3.0.x (the maintenance branch added when the 3.0 line was cut). Any new maintenance branch broke verification across every consumer until each SDK was patched.
Wildcarding the branch component doesn’t weaken the trust model: the upstream release.yml workflow’s own on.push.branches allowlist (currently main, 3.0.x, 2.6.x) is what determines which refs can produce a signature in the first place. Mirroring that list in every consumer’s regex was a maintenance liability that added no defense.
Cert subjects on past releases
For reference, here’s what each release’s certificate subject looked like:
| Release | Triggering ref | Cert subject (subject only, full URL prefix omitted) |
|---|
| v3.0.0 | main (initial 3.0 cut) | release.yml@refs/heads/main |
| v3.0.1 | 3.0.x (after the line was cut) | release.yml@refs/heads/3.0.x |
| v3.0.2 | 3.0.x | release.yml@refs/heads/3.0.x |
| v3.0.3 | 3.0.x | release.yml@refs/heads/3.0.x |
A future maintenance branch (e.g. 2.7.x) would add release.yml@refs/heads/2.7.x without needing any consumer change.
When verification is not available
Some releases legitimately ship without .sig/.crt:
- Pre-v3.0.0 (cosign signing wasn’t wired in yet). Treat as checksum-only. SDKs degrade to integrity-only verification rather than failing.
- Out-of-band republishes. If a tarball is regenerated outside the
release.yml workflow (e.g. a one-off rebuild), it has no Sigstore identity. The cosign sidecars will be absent. Treat as untrusted.
Consumers should distinguish “sidecars absent” (degrade to checksum-only) from “sidecars present but verification failed” (hard fail). Don’t conflate them — a present-but-invalid signature is a stronger negative signal than no signature at all.
SDK behavior
All three first-party SDKs use this regex when fetching protocol bundles:
| SDK | Verifies via |
|---|
@adcp/sdk (TypeScript) | scripts/sync-schemas.ts shells out to cosign verify-blob when sidecars are present |
adcp-client-python | scripts/sync_schemas.py does the same |
adcp-go | adcp/schemas/download.sh does the same |
If you maintain a fourth-party SDK, mirror the regex above. Stay away from literal-allowlist patterns — they will rot every time a new maintenance branch is cut.
Producer-side detail
If you’re contributing to the spec workflow itself: cosign signing happens during npm run version (chained from the sign-protocol-tarball.sh step) inside release.yml. The OIDC token is minted at signing time, so the cert subject reflects the trigger ref of that workflow run. Tag-based signing would require either:
- A second workflow that runs on
release: published and re-signs the tarball using the post-tag OIDC subject, or
- Restructuring the release pipeline so signing happens after
changeset tag and within a context where refs/tags/* is the active ref.
Today’s signed-from-branch shape is intentional — it lets every consumer verify a single canonical artifact without reasoning about tag-vs-branch identity. The regex’s refs/(heads|tags)/.* is forward-compat in case that changes.
See also