feat: supply-chain pinning (go commit hash + gh SHA256) via --pin on add/ensure/upgrade#79
Conversation
Add opt-in supply-chain verification for both go and gh runtimes: - go-runtime: pins the git commit hash from proxy.golang.org (Origin.Hash) and re-fetches it before go install to detect substitution attacks - gh-runtime: pins per-platform SHA256 archive digests (asset.GetDigest()) and verifies the downloaded archive before extraction New flag --pin on add, ensure, and upgrade writes the pin to .toolset.json. toolset sync reads the pin and enforces it on every fresh install. Tools without a pin install as before (backward compatible). Gaps fixed: - GAP-2: expose ResolvedModPath so proxy.golang.org accepts cmd/ tool paths - GAP-3/4: Upgrade clears stale pins; ensure/upgrade --pin re-pins at new version - GAP-5: injectable http.Client on go Runtime for testability Integration tests: task test:pin covers 8 scenarios (add/ensure/upgrade happy path + tamper-rejection for both runtimes)
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #79 +/- ##
==========================================
+ Coverage 31.77% 38.62% +6.85%
==========================================
Files 20 23 +3
Lines 1775 1908 +133
==========================================
+ Hits 564 737 +173
+ Misses 1142 1076 -66
- Partials 69 95 +26 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Fixes in this branch1. Deterministic
|
| Before | After |
|---|---|
| 1 goroutine | 0 goroutines |
2x time.After(1s) |
0 timers |
| ~2s minimum runtime | ~0ms runtime |
| Flaky under scheduler pressure | Always deterministic |
2. Lint fix: remove unused helpers in pin_test.go
pin_test.go had three helper functions scaffolded during development that were never called in any test: ptr, makeRelease, makeAsset. golangci-lint's unused linter flagged all three. Removed them along with the now-unused go-github import.
…ctx2 redundancy
- Fix goroutine leak in Test_fetchCommitHash_contextCancellation by using <-r.Context().Done()
instead of select {} to properly listen for cancellation
- Fix process-global side effect in Test_pin_flag_invalid_module by restoring working directory
with defer os.Chdir(original) to prevent parallel test interference
- Remove 7 redundant ctx2 declarations in runtime_go_test.go, reusing ctx instead
Fixes review feedback from PR kazhuravlev#79 code review (2026-06-12)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
b8846a1 to
465bf84
Compare
a614b22 to
635523f
Compare
635523f to
81a53cc
Compare
Unit tests for go-runtime and gh-runtime supply-chain pinning: go-runtime (runtime-go): - roundTripperFunc adapter for in-process HTTP mocking - fetchCommitHash full error-path suite: success, empty hash, non-200, JSON decode error, context cancellation, invalid module path - Test_Install_pinMismatch_rejected: end-to-end tamper detection via httptest.NewServer returning a mismatched commit hash - Test_Install_SHA256Verification_doesNotRejectMatchingHash: verifies the pin check passes when hashes agree (NotContains semantics explicit) - Test_GetPin_privateModule_returnsEmpty: fetchModuleFn seam returning IsPrivate:true asserts optional.Empty + no error gh-runtime (runtime-github-release): - newTestRuntime helper wires getAssetFn + downloadFn seams - Test_Install_pinMismatch_rejected / _SHA256Verification_doesNotRejectMatchingDigest - Test_Install_pinMissingPlatform, _downloadError, _corruptArchive - Test_GetPin_emptyDigest, _noAssetsForAnyPlatform, _releaseAPIError - Test_findPinnedAsset table tests; Test_computeSHA256 known-value tests - verify_test.go: computeSHA256 with known SHA256 vectors + empty file Shared: - workdir_test.go: New() missing spec, invalid JSON, TOOLSET_CACHE_DIR, deprecated Dir field, multiple runtimes - real_fs_test.go: deterministic TestLocks rewrite replacing timing-based goroutine test with context-cancellation approach; isolated lock files per subtest via os.CreateTemp Round 4 fixes applied in same commit: - Delete dead strings.Cut(!ok) branch in gh-runtime pin.go:35 (unreachable; parse() enforces owner/repo format upstream) - Pre-compile regex patterns outside asset loop in assets.go - Rename weak-assertion tests to make NotContains semantics explicit - Delete assertion-free Test_pin_flag_invalid_module Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
81a53cc to
dfec057
Compare
|
Thanks for the contribution. This is an important change, so I’d like to review it carefully over the next few days. |
kazhuravlev
left a comment
There was a problem hiding this comment.
Great work on this MR — the implementation is clean and well thought out. Thanks for the excellent contribution!
Summary
Adds opt-in supply-chain verification for both runtimes via a new
--pinflag onadd,ensure, andupgrade.What is pinned
goproxy.golang.org(Origin.Hash)go install— re-fetches from proxy to detect substitutionghasset.GetDigest())Usage
The pin is stored in
.toolset.jsonalongside the version.toolset syncenforces it on every fresh install. Tools without--pinare fully backward compatible.Key design decisions
pinunmarshal cleanly tooptional.EmptyResolvedModPath(module root, not thecmd/sub-path) to query the proxy — avoids 404s for tools likegolangci-lint/cmd/golangci-lintknownPlatforms(7 OS/arch combinations) at pin time and records a digest per platform, enabling cross-platform reproducibilityupgrade --pin: clears stale pin then immediately fetches the new oneTesting
go test ./...— all packages pass (unit tests for SHA256, mock proxy,findPinnedAsset, PIN mismatch rejection for both runtimes)task test:pin— 8 integration scenarios: add/ensure/upgrade happy path + tamper-rejection for both runtimes