Repository navigation
Add c1i upgrade: Sigstore-verified self-update from the C1 distribution center - #120
Merged
Merged
Conversation
leet-c1
force-pushed
the
feat/upgrade-command
branch
from
September 21, 2026 20:11
512cbaf to
a9ee22a
Compare
`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 of the C1 distribution center's release layout): 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>
Co-authored-by: c1-squire-dev[bot] <c1-squire-dev[bot]@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bump sigstore-go, sigstore, protobuf-specs and go-tuf to their latest releases, and their transitive modules with them. grpc is held at v1.83.2: v1.84.0 is still affected by GO-2026-6443, which v1.83.2 fixes. x/crypto and x/net move to releases that fix GO-2026-6303/6354/6355 and GO-2026-5942. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…and exit codes - Verification is now tested offline against the real v0.7.0 release and a snapshot of the Sigstore public-good trusted root: tampered bytes, a forged self-signed certificate, mismatched bundle signature/certificate, a corrupted SET, an integrated time past the certificate, and a trust root missing Fulcio, CT or Rekor each fail. A cmd-level httptest flow signs manifests with a throwaway Fulcio/CT/Rekor and checks that unsigned, tampered, wrong-semver and wrong-sha releases leave the binary untouched. - The certificate may come from any major tag of the release workflow, and must carry c1i's tag for the manifest's version, a push trigger and a GitHub-hosted runner. - A dist HTTP status keeps its exit code (404 4, 429 5, 5xx 6); signature, checksum, TUF, an unreachable dist and a refused redirect exit 8; a local replace or lock failure exits 1 and names the install directory. - `--check` prints a JSON report; pseudo-version builds don't self-replace; the lock no longer leaves a file next to the binary; the TUF trust root is fetched through the shared transport; `go env` runs with GOTOOLCHAIN=local from a neutral directory; confirm reads the command's input. Removes dead code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds an upgrade entry to the agents guide, cuts the README section to the examples and two paragraphs, and shortens the CHANGELOG entry. The unsigned index.json residual is restated: an origin can withhold an upgrade or offer a yanked release, but the rollback check stops it serving an older one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The snapshot trust root's base64 keys contain two runs shaped like C1 object ids. Allowlist those exact tokens rather than the file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ap dist 4xx to 8 - A directory flock that the filesystem can't take (EBADF, ENOLCK, EOPNOTSUPP/ENOTSUP, as on NFS) no longer aborts the upgrade; the rename is atomic either way. - The install directory's writability is checked before anything is downloaded, and the error names the directory once. - A dist 401, 403 or other 4xx exits 8: dist needs no auth and takes no caller input. 404, 429 and 5xx keep 4, 5 and 6. - Tests now cover the TUF fetch going through the client's Doer, the yank check, and the installed-version re-check under the lock. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ex residual Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rectory once The up-front writability check made the replace-failure path unreachable in tests; drive it with a directory where the binary should be. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
leet-c1
force-pushed
the
feat/upgrade-command
branch
from
October 5, 2026 23:13
f9b7864 to
9e527b8
Compare
c1i upgrade self-updater (Sigstore-verified) from the C1 distribution center… the downgrade rule Gives upgrade its own section in the agents guide, lists install_method values, says which dist failures exit 8, documents --dry-run, and notes that an https->http redirect is never followed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Creating the file (mode 0600), replacing url while keeping other keys, and the current behaviors worth knowing: comments are dropped, and an unparseable file is replaced rather than reported. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SaveToConfigFile now returns an error naming the file, and leaves it untouched, when it can't read or parse it. auth login has already stored its credential, so it still succeeds and warns that the URL wasn't saved. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A file holding only "---", "~" or "null" unmarshals to a nil map, and saving the URL then panicked after login had stored its credential. Such a file is now an empty config. The error for a file that isn't a YAML mapping now says so. Tests pin empty, whitespace and comment-only files and a file that can't be read. Also corrects the agents guide on what a source build prints, and re-wraps two edited paragraphs. Co-Authored-By: Claude Opus 5.5 <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), which installs a newer release from the C1 distribution center (dist.conductorone.com) and verifies it before replacing anything. Targeted for 0.9.0.Only a standalone binary is replaced in place. For Homebrew,
go install, and container installs it prints the right upgrade command and exits 0;--checkincludes it asupgrade_command. Windows and system-package installs are never self-replaced. Development builds (dev,(devel), Go pseudo-versions) refuse to replace themselves.Verification
ConductorOne/github-workflows/.github/workflows/release.yaml@refs/tags/v<major>and the GitHub Actions OIDC issuer. It must also carry source repoConductorOne/c1i, source refrefs/tags/v<manifest version>, triggerpush, and a GitHub-hosted runner.--debugand--max-retriesapply to it.The binary is replaced atomically: staged in the same directory,
fsync,rename. The writability of the install directory is checked before anything is downloaded. Concurrent upgrades are serialized with aflockon the install directory. On NFS-style filesystems that can't lock a directory, the upgrade proceeds unlocked, since the rename is atomic. The installed version is re-checked under the lock.Exit codes
Dist 404 → 4, 429 → 5, 5xx → 6. Any other dist 4xx, an unreachable dist or TUF host, a refused redirect, or a signature or checksum failure → 8. A local failure (directory not writable, replace failed) → 1, naming the install directory.
Known residual
index.json(channel targets and yank status) is not signed; only per-release manifests are. A compromised distribution origin can withhold upgrades or offer any signed release newer than yours, including a yanked or prerelease one. It can never deliver an unsigned build, or one older than the binary you run. Closing this needs a signed index on the dist side.Dependencies
sigstore-go,sigstore/sigstore,protobuf-specs,go-tuf/v2,golang.org/x/mod. All are at their latest release, exceptgrpc, held at v1.83.2 because v1.84.0 is still affected by GO-2026-6443.grpc-gatewaystays at v2.30.0 for the same reason, because v2.31.0 requires grpc v1.84.0.govulncheck -show verboseon linux, darwin and windows: 0 called and 0 imported vulnerabilities. One module-level finding, GO-2026-5932 (x/crypto/openpgp), has no fix and isn't imported.verify/tlog/bundlepackages (which pull in grpc). Hand-rolling those checks would save about 6.5 MB; we chose not to reimplement security-critical verification to save space.The shared transport also gains
WithMaxResponseBytes(opt-in), and now refuses anhttps → httpredirect.Also in this PR
~/.c1i.yamlno longer overwrites a file it can't read or parse. It leaves the file unchanged andauth loginprints a warning. The credential is still stored.~/.c1i.yamlholding only---,~ornullno longer crashes login. It used to panic on save; it's now treated as empty.SaveToConfigFilenow has tests, each confirmed by a mutant.Testing
httptestflow with a throwaway Fulcio CA, CT log and Rekor log. Unsigned, tampered, wrong-version, wrong-checksum and yanked releases all exit 8, with the binary byte-identical afterwards.dist.conductorone.com.--checkreturns JSON on all three channels.go install-shaped paths, plus dev builds, behave as described above.--max-retries 0.--debug.test -count=1 -shuffle=on, golangci-lint v2.14.0 (0 issues), gosec v2.29.0, govulncheck v1.8.0 ×3 GOOS,go mod tidy, and gitleaks all pass.Not tested live: tampered releases (covered offline), the interactive prompt, macOS, Windows, and container installs.
🤖 Generated with Claude Code