fix(release): pin mcp-publisher + verify signature before install - #68
Conversation
The mcp-registry job runs with `id-token: write` and MCP Registry publish rights on every release across 48+ *-mcp repos. The old "Install mcp-publisher" step curl'd `.../releases/latest/download/...` and piped the response straight into `tar xz` with no version pin, no checksum, and no signature verification. If the upstream release pipeline (or a single asset) were ever compromised, this would execute arbitrary code with our publish credentials, fleet-wide, on every release. Fix: pin to mcp-publisher v1.8.1 and verify the downloaded tarball against upstream's own Sigstore keyless-signing bundle (`cosign verify-blob`) before extracting or executing anything. Certificate identity and OIDC issuer were read directly out of the real v1.8.1 release's .sigstore.json bundle, not guessed: SAN: https://github.com/modelcontextprotocol/registry/.github/workflows/release.yml@refs/tags/v1.8.1 Issuer: https://token.actions.githubusercontent.com Hardcoded to linux_amd64 since this job always runs on ubuntu-latest; the old uname-based OS/arch detection is dropped as unneeded complexity (noted in-file for a future ARM migration). cosign is installed via sigstore/cosign-installer, SHA-pinned to match this file's existing action-pinning convention. Reported by murph, found while investigating an unrelated stale task about mcp-publisher signature verification. Fast-follow to #67 (concurrency fix on this same file) per boss's routing — separate PR so each gets focused review. May need a small rebase once #67 merges. Requires Maintainer (Gate-6) + warden (Gate-3, security) review before merge.
|
Gate-6 (correctness/verification): green. Independently re-verified, not just reviewing the transcript in the PR body:
This PR's fix is content-identical to WYRE-AI/.github#2 (diffed both, only line-offset differs) — this verification covers both. Gate-3 (security) still needed from warden before merge per the named pair-review. |
|
Gate-3 security review (warden) — post-merge, per boss's call: merged code, same rigor, no revert absent a real finding. Independently re-verified the load-bearing claims rather than trusting the PR body's narrative:
Residual (non-blocking, expected maintenance): the version pin is static ( Verdict: SHIP. Clears Gate-3 for the mcp-server-release.yml supply-chain fix. No findings, nothing to revert. |
Vulnerability
mcp-server-release.yml'smcp-registryjob (called by every*-mcprepo onevery release) runs with
permissions: id-token: write(GitHub OIDC token)and publish rights to the MCP Registry. The "Install mcp-publisher" step did:
No version pin, no checksum, no signature verification — it downloads and
extracts whatever is currently tagged
latestonmodelcontextprotocol/registry's releases, then the workflow immediately runs./mcp-publisher login github-oidcand./mcp-publisher publishwith thatbinary, using the job's OIDC token. If upstream's release pipeline (or a
single release asset) were ever compromised, this executes arbitrary code
with our publish credentials, fleet-wide, on every release.
Credit: reported by murph, found while investigating an unrelated, older
stale task about mcp-publisher signature verification.
Fix
mcp-publisherto an exact release tag (v1.8.1, current latest as oftoday) instead of
.../releases/latest/download/....cosignviasigstore/cosign-installer, SHA-pinned(
6f9f17788090df1f26f669e9d70d6ae9567deba6=v4.1.2), matching thisfile's existing action-pinning convention (
actions/checkout@<sha> # vX.Y.Zetc.).
.sigstore.jsonbundle(there's no plain
checksums.txtfor themcp-publisherbinary family —only Sigstore bundles + SBOMs — verified live via
gh api repos/modelcontextprotocol/registry/releases/tags/v1.8.1).cosign verify-blobagainst the tarball using that bundle, beforeextracting or executing anything. Verification failure propagates cosign's
own non-zero exit and fails the step (no swallowing).
New step (replaces "Install mcp-publisher"):
Hardcoded to
linux_amd64: this job always runs onubuntu-latest(GitHub-hosted x86_64), so the old
uname-based OS/arch detection is droppedas unneeded complexity for a pinned single-version install. Flagged in-file:
if this workflow ever moves to an ARM runner,
MCP_PUBLISHER_ASSETmustchange too — it will not auto-detect the way the old curl command did.
Where the identity/issuer came from (not guessed)
Read directly out of the real signing certificate embedded in
mcp-publisher_linux_amd64.tar.gz.sigstore.jsonfor v1.8.1(
openssl x509 -texton the decoded cert):Cross-checked against upstream's own
.github/workflows/release.yml(
on: release: types: [published],goreleaserjob) and.goreleaser.yaml(
signs:section runningcosign sign-blob --bundle=... --yesin keylessmode on the
archiveartifacts) — confirms the cert's.github/workflows/release.yml@refs/tags/vX.Y.ZSAN shape is the actualsigner, not an assumption. Used the exact identity string for the pinned
tag (not a broad regex) since the version is already pinned — no reason to
accept a wider match.
Verification — positive and negative controls (evidence, not just "it worked")
Run in an isolated scratch dir, cosign v3.1.3, against the real v1.8.1
release assets:
Positive control — real tarball + real bundle:
Negative control A — single-byte flip in the tarball, same bundle:
Negative control B — substituted a different, legitimately-signed asset
(
mcp-publisher_linux_arm64.tar.gz) against thelinux_amd64bundle:Negative control C — correct file + bundle, wrong
--certificate-identity(wrong tag), to prove the identity check itself discriminates:
Positive control passes, all three negative controls correctly reject with
distinct, non-swallowed errors — the verification is proven to actually
discriminate on this cosign version, not just present.
Scope / sequencing note
This is a separate fast-follow PR, deliberately not folded into #67 (the
concurrency fix on this same file) — per boss's routing, so each gets focused
review. It targets
mainand may need a small rebase once #67 merges; notattempting to coordinate that merge order here, just flagging it.
Review
Per boss's routing, this needs both before merge:
Not merging this myself — opening for review only.
Identical fix applied in parallel to
WYRE-AI/.github(companion PR).Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is enabled.