feat: add c1i upgrade self-updater (Sigstore-verified) from the C1 distribution center - #120
Open
leet-c1 wants to merge 3 commits into
Open
feat: add c1i upgrade self-updater (Sigstore-verified) from the C1 distribution center#120leet-c1 wants to merge 3 commits into
c1i upgrade self-updater (Sigstore-verified) from the C1 distribution center#120leet-c1 wants to merge 3 commits into
Conversation
`c1i upgrade` (alias `update`) checks for and installs a newer release. Discovery uses dist.conductorone.com's public release interface (the index.json channels + per-version manifest.json documented in baton-admin's dist-release RFC): the `stable` channel by default, `latest`/`preview` via --channel. No C1 CLI consumes this yet, so the small stable subset of the schema is hand-rolled in internal/selfupdate rather than importing the canonical protobuf from github-workflows. Flow: read the channel's target version, compare to the build-info version, and for a standalone binary download the GOOS-GOARCH asset, verify its manifest SHA-256, and atomically replace the running executable (temp file in the same directory, then rename — POSIX allows replacing a running binary). `--check` reports without changing anything; `--dry-run` previews the exact download and target; `--yes` skips the prompt (required when stdin isn't a tty). Install-method aware: a Homebrew (Cellar), `go install` (GOBIN/GOPATH), or container-image install is NOT self-replaced — upgrade prints the right command for it. Windows (a running .exe can't be swapped; the channel is an MSI) is pointed at the download. Fetches go through internal/transport, so --debug/--max-retries and the c1i user-agent apply and the file is not a new HTTP-bypass. sha256 only for v1; the manifest's Sigstore signature/SLSA provenance are a fast-follow. No new dependencies. Live-validated against real dist: --check distinguishes stable (v0.6.0) from latest (v0.7.0), and --dry-run resolves the real v0.7.0 linux-amd64 artifact whose sha256 matches the published checksum. Unit tests cover version compare, install detection, index/manifest parsing, the SPA-shell guard, sha256 verify, tar.gz/zip extraction, atomic replace, and end-to-end apply including the checksum-mismatch abort. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Addresses the adversarial-review findings on `c1i upgrade`. Verification is now two layers: the release manifest's keyless Sigstore signature is checked against a PINNED identity before it is trusted, and the manifest's per-asset SHA-256 then anchors the downloaded bytes. Sigstore (internal/selfupdate/verify.go): pins the SAN (github.com/ConductorOne/github-workflows/.github/workflows/release.yaml@refs/tags/v4) and OIDC issuer (token.actions.githubusercontent.com). Verifies, all mandatory: the cert identity matches the pin, the signature is valid over exactly the raw manifest bytes, the cert chains to a Fulcio root, and its SCT verifies against the trust root's CT logs. Local checks run first so a wrong identity / tampered manifest fails offline. The detached manifest .sig/.cert carry no Rekor entry, so Rekor is not enforced for the manifest signature and the chain is validated at the cert's NotBefore (Fulcio certs are ~10 min, so time.Now() would fail every past release) -- documented in-code; trust rests on pinned identity + SCT + SHA-256. A compromised dist can no longer install an attacker binary: it cannot mint a Fulcio cert for ConductorOne's workflow identity. Hardening: href/manifest/sig/cert URLs are pinned to https + the dist host (no fetch-from-arbitrary-host); transport gained WithMaxResponseBytes and the download/metadata reads are bounded (no pre-verification OOM); the redirect guard refuses an https->http downgrade; replaceExecutable fsyncs the staged file and its dir; CompareVersions does proper semver prerelease ordering (rc.10 > rc.2); fromZip requires a regular file; container detection adds Podman's /run/.containerenv; manifest.Semver is checked == target; and the confirm prompt names the exact binary path being replaced. Deps: sigstore-go v1.2.1, sigstore v1.10.8 (+ transitive) -- pinned past GO-2026-6162 / GO-2026-6061; govulncheck clean. Tests: offline failure paths (bad base64, non-PEM, tampered bytes, identity/issuer mismatch) plus a live integration test verifying the real v0.7.0 manifest against the pinned identity. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…sion check Addresses the adversarial re-review of the Sigstore work. Both red-teams found the crypto sound (no attacker-binary RCE under full dist compromise) and the hardening genuine/regression-free. Two residuals closed here: - The one Medium finding: index.json (channel + yank status) is not signed, only the per-release manifest is, so a compromised distribution origin could steer a user to a different but authentic ConductorOne-signed release (older-but-not- below-current, or yanked) — never to an unsigned/third-party binary. c1i can't fully close this without a signed index (a dist-side change); documented in the README and at the yank check so `yanked` is understood as best-effort, not a hard security boundary. - A low fail-closed brittleness: manifest.Semver == target was an exact string compare (a v0.7.0 vs 0.7.0 skew would falsely reject); now compared as semver. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds
c1i upgrade(aliasupdate) — a self-updater that pulls releases from the C1 distribution center (dist.conductorone.com), with install-method awareness and cryptographic verification.How it verifies (two layers)
.sig/.certare checked against a pinned identity — the ConductorOne reusable release workflow (SAN URI…/release.yaml@refs/tags/v4) and the GitHub Actions OIDC issuer. Chain validation is anchored at the certificate'sNotBefore(Fulcio certs are ~10-min short-lived), and the embedded SCT is mandatory. Local checks (identity, signature) run before any network round-trip.Only after both pass is the running binary replaced, via an atomic same-dir stage →
Sync→chmod→rename(POSIX permits renaming over a running executable).Install-method awareness
Only a standalone downloaded binary is replaced in place. Homebrew,
go install, container image, and Windows installs are detected and print the correct upgrade command instead of self-replacing.Hardening (shared transport)
WithMaxResponseBytesbounds response bodies (metadata 8 MiB, artifact 200 MiB) so a hostile endpoint can't OOM the process. Additive; existing callers unchanged (unbounded by default).https → httpscheme downgrade (credentials over cleartext);http → httpsstill allowed.Known residual (documented, not a hard boundary)
index.json(channel targets + yank status) is not signed — only per-release manifests are. A compromised distribution origin could therefore steer a client to a different but authentic, ConductorOne-signed release (an older one, down to the current version — no further — or one marked yanked), but never to an unsigned or third-party binary. The monotonicity gate (cmp < 0) plus the signature bound the blast radius; the README and an in-code comment call this out. Closing it fully needs a signed index (a dist-side change).Testing
httptestend-to-end coverage acrossinternal/selfupdate,internal/transport, andcmd, including a live test that verifies the real published v0.7.0 manifest signature against the pinned identity, and negative tests (bad base64, non-PEM cert, tampered bytes, wrong identity).dist.conductorone.com: a genuine self-replace v0.5.2 → v0.7.0 where the on-disk binary's SHA-256 matched the published archive byte-for-byte; plus the Homebrew /go installrefusal branches, already-latest / ahead-of-channel messages, and bad-channel (exit 2).build,vet,test(incl.-shuffle),golangci-lint(0),gosec(0),gitleaks(clean),govulncheck(clean — required asigstore-gov1.2.1 +grpcv1.82.1 bump to clear two advisories).Reviewed independently (Sigstore trust chain, download/apply, URL pinning, control flow, transport hardening) — no blocking findings.
🤖 Generated with Claude Code