From 5640739a6716b0177c5087a9e7963c55331f539f Mon Sep 17 00:00:00 2001 From: Piotr Sikora Date: Thu, 29 Oct 2020 21:52:40 +0000 Subject: [PATCH 01/13] Authenticate releases using the embedded verification key. Fixes #15. Signed-off-by: Piotr Sikora --- MODULE.bazel | 1 + core/core.go | 2 +- core/repositories.go | 12 ++++----- go.mod | 1 + go.sum | 2 ++ httputil/BUILD | 1 + httputil/httputil.go | 36 ++++++++++++++++++++++++- repositories/gcs.go | 60 +++++++++++++++++++++++++++++++++++++++--- repositories/github.go | 2 +- 9 files changed, 105 insertions(+), 12 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index 2d2fb43f..373e9624 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -27,5 +27,6 @@ use_repo( "com_github_gofrs_flock", "com_github_hashicorp_go_version", "com_github_mitchellh_go_homedir", + "org_golang_x_crypto", "org_golang_x_term", ) diff --git a/core/core.go b/core/core.go index 2fcaad8e..4a5aade0 100644 --- a/core/core.go +++ b/core/core.go @@ -1395,7 +1395,7 @@ func downloadInstallerToCAS(installerURL, bazeliskHome string, config config.Con tmpInstallerFile := fmt.Sprintf("%x-installer", tmpInstallerBytes) // Download the installer - installerPath, err := httputil.DownloadBinary(installerURL, temporaryDownloadDir, tmpInstallerFile, config) + installerPath, err := httputil.DownloadBinary(installerURL, "", "", temporaryDownloadDir, tmpInstallerFile, config) if err != nil { return "", fmt.Errorf("failed to download installer: %w", err) } diff --git a/core/repositories.go b/core/repositories.go index 33f8f4d1..2e5998d5 100644 --- a/core/repositories.go +++ b/core/repositories.go @@ -72,10 +72,10 @@ type RollingRepo interface { // Repositories offers access to different types of Bazel repositories, mainly for finding and downloading the correct version of Bazel. type Repositories struct { - LTS LTSRepo - Fork ForkRepo - Commits CommitRepo - Rolling RollingRepo + LTS LTSRepo + Fork ForkRepo + Commits CommitRepo + Rolling RollingRepo supportsBaseOrFormatURL bool } @@ -219,7 +219,7 @@ func (r *Repositories) DownloadFromBaseURL(baseURL, version, destDir, destFile s } url := fmt.Sprintf("%s/%s/%s", baseURL, version, srcFile) - return httputil.DownloadBinary(url, destDir, destFile, config) + return httputil.DownloadBinary(url, "", "", destDir, destFile, config) } // BuildURLFromFormat returns a Bazel download URL based on formatURL. @@ -282,7 +282,7 @@ func (r *Repositories) DownloadFromFormatURL(config config.Config, formatURL, ve return "", err } - return httputil.DownloadBinary(url, destDir, destFile, config) + return httputil.DownloadBinary(url, "", "", destDir, destFile, config) } // CreateRepositories creates a new Repositories instance with the given repositories. Any nil repository will be replaced by a dummy repository that raises an error whenever a download is attempted. diff --git a/go.mod b/go.mod index fae3b6c6..81ad1819 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/hashicorp/go-version v1.7.0 github.com/mitchellh/go-homedir v1.1.0 golang.org/x/term v0.39.0 + golang.org/x/crypto v0.45.0 ) require golang.org/x/sys v0.40.0 // indirect diff --git a/go.sum b/go.sum index c21c80a3..c86a3234 100644 --- a/go.sum +++ b/go.sum @@ -12,6 +12,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= diff --git a/httputil/BUILD b/httputil/BUILD index c222fc51..9e365968 100644 --- a/httputil/BUILD +++ b/httputil/BUILD @@ -17,6 +17,7 @@ go_library( "//httputil/progress", "@com_github_bgentry_go_netrc//netrc", "@com_github_mitchellh_go_homedir//:go-homedir", + "@org_golang_x_crypto//openpgp" ], ) diff --git a/httputil/httputil.go b/httputil/httputil.go index 0103d29b..18bef4d9 100644 --- a/httputil/httputil.go +++ b/httputil/httputil.go @@ -5,6 +5,7 @@ import ( b64 "encoding/base64" "errors" "fmt" + "golang.org/x/crypto/openpgp" "io" "log" "math/rand" @@ -14,6 +15,7 @@ import ( "path/filepath" "regexp" "strconv" + "strings" "time" netrc "github.com/bgentry/go-netrc/netrc" @@ -187,7 +189,7 @@ func tryFindNetrcFileCreds(host string) (string, error) { } // DownloadBinary downloads a file from the given URL into the specified location, marks it executable and returns its full path. -func DownloadBinary(originURL, destDir, destFile string, config config.Config) (string, error) { +func DownloadBinary(originURL, signatureURL, verificationKey, destDir, destFile string, config config.Config) (string, error) { err := os.MkdirAll(destDir, 0755) if err != nil { return "", fmt.Errorf("could not create directory %s: %v", destDir, err) @@ -247,6 +249,38 @@ func DownloadBinary(originURL, destDir, destFile string, config config.Config) ( return "", fmt.Errorf("could not chmod file %s: %v", tmpfile.Name(), err) } + if signatureURL != "" && verificationKey != "" { + signature, err := get(signatureURL, "") + if err != nil { + return "", fmt.Errorf("HTTP GET %s failed: %v", signatureURL, err) + } + defer signature.Body.Close() + + if signature.StatusCode != 200 { + return "", fmt.Errorf("HTTP GET %s failed with error %v", signatureURL, signature.StatusCode) + } + + keys, err := openpgp.ReadArmoredKeyRing(strings.NewReader(verificationKey)) + if err != nil { + return "", fmt.Errorf("failed to load the embedded Verification Key") + } + + if len(keys) != 1 { + return "", fmt.Errorf("failed to load the embedded Verification Key") + } + + tmpfile.Seek(0, io.SeekStart) + + entity, err := openpgp.CheckDetachedSignature(keys, tmpfile, signature.Body) + if err != nil { + return "", fmt.Errorf("failed to verify the downloaded file using signature from %s", signatureURL) + } + + for _, identity := range entity.Identities { + log.Printf("Signed by %s", identity.Name) + } + } + tmpfile.Close() err = os.Rename(tmpfile.Name(), destinationPath) if err != nil { diff --git a/repositories/gcs.go b/repositories/gcs.go index a7fcda5f..8f3f404a 100644 --- a/repositories/gcs.go +++ b/repositories/gcs.go @@ -22,6 +22,60 @@ const ( ltsBaseURL = "https://releases.bazel.build" commitBaseURL = "https://storage.googleapis.com/bazel-builds/artifacts" lastGreenCommitURL = "https://storage.googleapis.com/bazel-builds/last_green_commit/github.com/bazelbuild/bazel.git/publish-bazel-binaries" + verificationKey = ` +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBFdEmzkBEACzj8tMYUau9oFZWNDytcQWazEO6LrTTtdQ98d3JcnVyrpT16yg +I/QfGXA8LuDdKYpUDNjehLtBL3IZp4xe375Jh8v2IA2iQ5RXGN+lgKJ6rNwm15Kr +qYeCZlU9uQVpZuhKLXsWK6PleyQHjslNUN/HtykIlmMz4Nnl3orT7lMI5rsGCmk0 +1Kth0DFh8SD9Vn2G4huddwxM8/tYj1QmWPCTgybATNuZ0L60INH8v6+J2jJzViVc +NRnR7mpouGmRy/rcr6eY9QieOwDou116TrVRFfcBRhocCI5b6uCRuhaqZ6Qs28Bx +4t5JVksXJ7fJoTy2B2s/rPx/8j4MDVEdU8b686ZDHbKYjaYBYEfBqePXScp8ndul +XWwS2lcedPihOUl6oQQYy59inWIpxi0agm0MXJAF1Bc3ToSQdHw/p0Y21kYxE2pg +EaUeElVccec5poAaHSPprUeej9bD9oIC4sMCsLs7eCQx2iP+cR7CItz6GQtuZrvS +PnKju1SKl5iwzfDQGpi6u6UAMFmc53EaH05naYDAigCueZ+/2rIaY358bECK6/VR +kyrBqpeq6VkWUeOkt03VqoPzrw4gEzRvfRtLj+D2j/pZCH3vyMYHzbaaXBv6AT0e +RmgtGo9I9BYqKSWlGEF0D+CQ3uZfOyovvrbYqNaHynFBtrx/ZkM82gMA5QARAQAB +tEdCYXplbCBEZXZlbG9wZXIgKEJhemVsIEFQVCByZXBvc2l0b3J5IGtleSkgPGJh +emVsLWRldkBnb29nbGVncm91cHMuY29tPokCVQQTAQgAPwIbAwYLCQgHAwIGFQgC +CQoLBBYCAwECHgECF4AWIQRxodDvz+tigf0EN8k9WRm0SEV+4AUCXsoWGgUJC0fh +4QAKCRA9WRm0SEV+4NDCD/9c5rhZREBlikdi5QYRq1YOkwzJLXFoVe0FonEwMuWK +fQzT/rIwyh14tssptU5+eXwTEXL0ZDskgzvrFSpzjQZzcSG/gzNCATNfrZpC2nfE +SxMKOeIwQedn26YIHCI8s9tEQ7BSvfBfJgqfIo3IURhmfzNMj+qszca+3IDYAlAy +8lxUVbJcIQ0apnAdnIadtydzca56mMN7ma+btddaWLpAdyfUvQ/Zsx3TYYLF7inQ +km0JpzISN0fGngzGNDGNmtHNhCdSpyfkr+7fvpbKAYkSH7uZ1AIPDyHdLIwDQnX2 +kbLRkxKncKGSDhUSdlJTl0x36cU+xmgO15FFdOyk3BUfrlfDrgXIBjeX8KNh9TV6 +HgFFR/mNONoJ93ZvZQNO2s1gbPZJe3VJ1Q5PMLW1sdl8q8JthBwT/5TJ1k8E5VYj +jAc8dl+RAALxqj+eo5xI45o1FdV5s1aGDjbwFoCIhGCy2zaog1q5wnhmEptAAD0S +TVbJSpwNiLlPIcGVaCjXp8Ow3SzOGTRKIjFTO/I6FiSJOpgfri07clXmnb4ETjou +mUdglg8/8nQ120zHEOqoSzzIbTNUDjNZY8SuY6Ig3/ObQ/JAFS0i6h74KLfXUZzn +uETY7KURLdyPAhL37Hb9FDhvkJCUO/l6eqDh9jk1JjB7Cvb7hEvnbvDrr2hWNAL7 +RrkCDQRXRJs5ARAA55/1VBlDpV/ElUyLmRyPCz/V+msHdinyw4Mv5DJQupuZwlMy +vxPPzc7GmsIfk1zuOzDWirNs22r43ak6dsAvpcU+iVBi46MqUcbNtC+kfxlKiToD +PCs82rdfCgHT7XYDzrCWlqNQ9++BqM2OYRIxyEucizeofWPlrJUgKvu8fWLVZ6bY +n4L/PqAhobhuSjRcoB5Tp81hGa4cscKIGIqhymfnguaY8viJ83tHPUqQJoApNPy8 +q1pWHSDV6zBv71beqV2b6cBzp7VqNYOIuqE6ZNBFWuCG3zRc9ia2/bHxx2TGAQJt +PpPzitm0xkB3GGN06YnnSCE+f2j+7F0IO6uFlSy7ho0PoSFbDgR91kJK3S0ZBZx4 +H21cIpWWBzf9Nd1M4H3O7KhnGSZDq6+tXZ9/F/ZUvCZHpQlJewDPY9315Ymacf5C +Zk8xeE5UUIxFMdOxF8B7Itb6rbFWv+tzWdX/0/M8/b0ZJhVvngWzuh/agdS4E5an +f7ahGWM96jPRIQEb9DRN2YGp9hOiX2sZqkhxE5zWqD2gdXp2ZAxMCTHf4ijzOVsO +nde7b5BqC0JL73gNwf1iOHyCAzqGiFfah8/odBTDhMsdVMsjSIxzcwlwRnzy+hBs +dYpP19ieJCMoERJTbUgSspPdhY/Y4ChzlFHjiAKYT6vXiYcKS04stCtHqwEAEQEA +AYkCPAQYAQgAJgIbDBYhBHGh0O/P62KB/QQ3yT1ZGbRIRX7gBQJeyhYlBQkLR+Hs +AAoJED1ZGbRIRX7g3Y8P/iuOAHmyCMeSELvUs9ZvLYJKGzmz67R8fJSmgst/Bs3p +dWCAjGE56M6UgZzHXK+fBRWFPDOXT64XNq0UIG7tThthwe4Gdvg/5rWG61Pe/vCZ +2FkMAlEMkuufZYMcw9jItHMKLcYyW/jtN9EzCX+vM6SZlu4o8la5rCIBEaiKfzft +a/dRMjW+RqQnU31NQCDAy3zoGUCQumJtv3GVbMYHIrRZua2yyNo9Iborh2SVdBbK +v9WJKH4JcCHd0/XDGdys6EXeATIIRxchumkmxpIg87OhsC0n5yuH1FnFIFQEjbYX +bb46F7ZFT+8Tov+lgMEw4CZmps4uvvZlKbIH4Zi/ULiobwvm2ad3nejWICmGmHYz +ro6t08hdcY6GnOzCpDwx9yHechMCkU3KEE98nb/CxcmA4VzDHudTJe7o0OyaSarh +6D5WcXf7D9FfcKmUD9xaCsfXh66OCksMVGE1JctrO1wQTF2jTdTUq7mmi30tlM+o +JjVk65OSOd4JYol8auzE4oXOfsNzXbyvj7WzM1v5m7C45jOL+Ly7I3IUzZNfF41J +AMmSd73EOoR9YH4qTrL3jx69Ekf7ww70Qea5enLE8xUgQfGTOaEHxkFcEovmzv54 +6IVe083iK8alXD/9OUTaDY9NwMnOn1K1aU2XOfliGGLgwwaHg+wVFh5rZIHsDl7v +=Embu +-----END PGP PUBLIC KEY BLOCK----- +` ) // GCSRepo represents a Bazel repository on Google Cloud Storage that contains Bazel releases, release candidates and Bazel binaries built at arbitrary commits. @@ -196,7 +250,7 @@ func (gcs *GCSRepo) DownloadLTS(version, destDir, destFile string, config config } url := fmt.Sprintf("%s/%s/%s/%s", ltsBaseURL, baseVersion, folder, srcFile) - return httputil.DownloadBinary(url, destDir, destFile, config) + return httputil.DownloadBinary(url, url+".sig", verificationKey, destDir, destFile, config) } // CommitRepo @@ -225,7 +279,7 @@ func (gcs *GCSRepo) DownloadAtCommit(commit, destDir, destFile string, config co return "", err } url := fmt.Sprintf("%s/%s/%s/bazel", commitBaseURL, platform, commit) - return httputil.DownloadBinary(url, destDir, destFile, config) + return httputil.DownloadBinary(url, "", "", destDir, destFile, config) } // RollingRepo @@ -274,5 +328,5 @@ func (gcs *GCSRepo) DownloadRolling(version, destDir, destFile string, config co releaseVersion := strings.Split(version, "-")[0] url := fmt.Sprintf("%s/%s/rolling/%s/%s", ltsBaseURL, releaseVersion, version, srcFile) - return httputil.DownloadBinary(url, destDir, destFile, config) + return httputil.DownloadBinary(url, "", "", destDir, destFile, config) } diff --git a/repositories/github.go b/repositories/github.go index c5c3f4b4..9d8a7b87 100644 --- a/repositories/github.go +++ b/repositories/github.go @@ -91,5 +91,5 @@ func (gh *GitHubRepo) DownloadVersion(fork, version, destDir, destFile string, c return "", err } url := fmt.Sprintf(urlPattern, fork, version, filename) - return httputil.DownloadBinary(url, destDir, destFile, config) + return httputil.DownloadBinary(url, "", "", destDir, destFile, config) } From 5877133bc04417a051abc75b7d5bb9579d4203dc Mon Sep 17 00:00:00 2001 From: Valentin Grigorev Date: Mon, 19 Jan 2026 13:00:31 +0100 Subject: [PATCH 02/13] Check authenticity for all download URLs --- core/core.go | 2 +- core/repositories.go | 4 +-- httputil/httputil.go | 57 ++++++++++++++++++++++++++++++++++++++++++ repositories/gcs.go | 56 +---------------------------------------- repositories/github.go | 2 +- 5 files changed, 62 insertions(+), 59 deletions(-) diff --git a/core/core.go b/core/core.go index 4a5aade0..7cfc0ed7 100644 --- a/core/core.go +++ b/core/core.go @@ -1395,7 +1395,7 @@ func downloadInstallerToCAS(installerURL, bazeliskHome string, config config.Con tmpInstallerFile := fmt.Sprintf("%x-installer", tmpInstallerBytes) // Download the installer - installerPath, err := httputil.DownloadBinary(installerURL, "", "", temporaryDownloadDir, tmpInstallerFile, config) + installerPath, err := httputil.DownloadBinary(installerURL, installerURL+".sig", httputil.VerificationKey, temporaryDownloadDir, tmpInstallerFile, config) if err != nil { return "", fmt.Errorf("failed to download installer: %w", err) } diff --git a/core/repositories.go b/core/repositories.go index 2e5998d5..1c1c6bb0 100644 --- a/core/repositories.go +++ b/core/repositories.go @@ -219,7 +219,7 @@ func (r *Repositories) DownloadFromBaseURL(baseURL, version, destDir, destFile s } url := fmt.Sprintf("%s/%s/%s", baseURL, version, srcFile) - return httputil.DownloadBinary(url, "", "", destDir, destFile, config) + return httputil.DownloadBinary(url, url+".sig", httputil.VerificationKey, destDir, destFile, config) } // BuildURLFromFormat returns a Bazel download URL based on formatURL. @@ -282,7 +282,7 @@ func (r *Repositories) DownloadFromFormatURL(config config.Config, formatURL, ve return "", err } - return httputil.DownloadBinary(url, "", "", destDir, destFile, config) + return httputil.DownloadBinary(url, url+".sig", httputil.VerificationKey, destDir, destFile, config) } // CreateRepositories creates a new Repositories instance with the given repositories. Any nil repository will be replaced by a dummy repository that raises an error whenever a download is attempted. diff --git a/httputil/httputil.go b/httputil/httputil.go index 18bef4d9..0a11df8f 100644 --- a/httputil/httputil.go +++ b/httputil/httputil.go @@ -34,6 +34,63 @@ var ( // RetryClock is used for waiting between HTTP request retries. RetryClock = Clock(&realClock{}) + + // VerificationKey is the public PGP key used to verify Bazel binary signatures. + VerificationKey = ` +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBFdEmzkBEACzj8tMYUau9oFZWNDytcQWazEO6LrTTtdQ98d3JcnVyrpT16yg +I/QfGXA8LuDdKYpUDNjehLtBL3IZp4xe375Jh8v2IA2iQ5RXGN+lgKJ6rNwm15Kr +qYeCZlU9uQVpZuhKLXsWK6PleyQHjslNUN/HtykIlmMz4Nnl3orT7lMI5rsGCmk0 +1Kth0DFh8SD9Vn2G4huddwxM8/tYj1QmWPCTgybATNuZ0L60INH8v6+J2jJzViVc +NRnR7mpouGmRy/rcr6eY9QieOwDou116TrVRFfcBRhocCI5b6uCRuhaqZ6Qs28Bx +4t5JVksXJ7fJoTy2B2s/rPx/8j4MDVEdU8b686ZDHbKYjaYBYEfBqePXScp8ndul +XWwS2lcedPihOUl6oQQYy59inWIpxi0agm0MXJAF1Bc3ToSQdHw/p0Y21kYxE2pg +EaUeElVccec5poAaHSPprUeej9bD9oIC4sMCsLs7eCQx2iP+cR7CItz6GQtuZrvS +PnKju1SKl5iwzfDQGpi6u6UAMFmc53EaH05naYDAigCueZ+/2rIaY358bECK6/VR +kyrBqpeq6VkWUeOkt03VqoPzrw4gEzRvfRtLj+D2j/pZCH3vyMYHzbaaXBv6AT0e +RmgtGo9I9BYqKSWlGEF0D+CQ3uZfOyovvrbYqNaHynFBtrx/ZkM82gMA5QARAQAB +tEdCYXplbCBEZXZlbG9wZXIgKEJhemVsIEFQVCByZXBvc2l0b3J5IGtleSkgPGJh +emVsLWRldkBnb29nbGVncm91cHMuY29tPokCVQQTAQgAPwIbAwYLCQgHAwIGFQgC +CQoLBBYCAwECHgECF4AWIQRxodDvz+tigf0EN8k9WRm0SEV+4AUCXsoWGgUJC0fh +4QAKCRA9WRm0SEV+4NDCD/9c5rhZREBlikdi5QYRq1YOkwzJLXFoVe0FonEwMuWK +fQzT/rIwyh14tssptU5+eXwTEXL0ZDskgzvrFSpzjQZzcSG/gzNCATNfrZpC2nfE +SxMKOeIwQedn26YIHCI8s9tEQ7BSvfBfJgqfIo3IURhmfzNMj+qszca+3IDYAlAy +8lxUVbJcIQ0apnAdnIadtydzca56mMN7ma+btddaWLpAdyfUvQ/Zsx3TYYLF7inQ +km0JpzISN0fGngzGNDGNmtHNhCdSpyfkr+7fvpbKAYkSH7uZ1AIPDyHdLIwDQnX2 +kbLRkxKncKGSDhUSdlJTl0x36cU+xmgO15FFdOyk3BUfrlfDrgXIBjeX8KNh9TV6 +HgFFR/mNONoJ93ZvZQNO2s1gbPZJe3VJ1Q5PMLW1sdl8q8JthBwT/5TJ1k8E5VYj +jAc8dl+RAALxqj+eo5xI45o1FdV5s1aGDjbwFoCIhGCy2zaog1q5wnhmEptAAD0S +TVbJSpwNiLlPIcGVaCjXp8Ow3SzOGTRKIjFTO/I6FiSJOpgfri07clXmnb4ETjou +mUdglg8/8nQ120zHEOqoSzzIbTNUDjNZY8SuY6Ig3/ObQ/JAFS0i6h74KLfXUZzn +uETY7KURLdyPAhL37Hb9FDhvkJCUO/l6eqDh9jk1JjB7Cvb7hEvnbvDrr2hWNAL7 +RrkCDQRXRJs5ARAA55/1VBlDpV/ElUyLmRyPCz/V+msHdinyw4Mv5DJQupuZwlMy +vxPPzc7GmsIfk1zuOzDWirNs22r43ak6dsAvpcU+iVBi46MqUcbNtC+kfxlKiToD +PCs82rdfCgHT7XYDzrCWlqNQ9++BqM2OYRIxyEucizeofWPlrJUgKvu8fWLVZ6bY +n4L/PqAhobhuSjRcoB5Tp81hGa4cscKIGIqhymfnguaY8viJ83tHPUqQJoApNPy8 +q1pWHSDV6zBv71beqV2b6cBzp7VqNYOIuqE6ZNBFWuCG3zRc9ia2/bHxx2TGAQJt +PpPzitm0xkB3GGN06YnnSCE+f2j+7F0IO6uFlSy7ho0PoSFbDgR91kJK3S0ZBZx4 +H21cIpWWBzf9Nd1M4H3O7KhnGSZDq6+tXZ9/F/ZUvCZHpQlJewDPY9315Ymacf5C +Zk8xeE5UUIxFMdOxF8B7Itb6rbFWv+tzWdX/0/M8/b0ZJhVvngWzuh/agdS4E5an +f7ahGWM96jPRIQEb9DRN2YGp9hOiX2sZqkhxE5zWqD2gdXp2ZAxMCTHf4ijzOVsO +nde7b5BqC0JL73gNwf1iOHyCAzqGiFfah8/odBTDhMsdVMsjSIxzcwlwRnzy+hBs +dYpP19ieJCMoERJTbUgSspPdhY/Y4ChzlFHjiAKYT6vXiYcKS04stCtHqwEAEQEA +AYkCPAQYAQgAJgIbDBYhBHGh0O/P62KB/QQ3yT1ZGbRIRX7gBQJeyhYlBQkLR+Hs +AAoJED1ZGbRIRX7g3Y8P/iuOAHmyCMeSELvUs9ZvLYJKGzmz67R8fJSmgst/Bs3p +dWCAjGE56M6UgZzHXK+fBRWFPDOXT64XNq0UIG7tThthwe4Gdvg/5rWG61Pe/vCZ +2FkMAlEMkuufZYMcw9jItHMKLcYyW/jtN9EzCX+vM6SZlu4o8la5rCIBEaiKfzft +a/dRMjW+RqQnU31NQCDAy3zoGUCQumJtv3GVbMYHIrRZua2yyNo9Iborh2SVdBbK +v9WJKH4JcCHd0/XDGdys6EXeATIIRxchumkmxpIg87OhsC0n5yuH1FnFIFQEjbYX +bb46F7ZFT+8Tov+lgMEw4CZmps4uvvZlKbIH4Zi/ULiobwvm2ad3nejWICmGmHYz +ro6t08hdcY6GnOzCpDwx9yHechMCkU3KEE98nb/CxcmA4VzDHudTJe7o0OyaSarh +6D5WcXf7D9FfcKmUD9xaCsfXh66OCksMVGE1JctrO1wQTF2jTdTUq7mmi30tlM+o +JjVk65OSOd4JYol8auzE4oXOfsNzXbyvj7WzM1v5m7C45jOL+Ly7I3IUzZNfF41J +AMmSd73EOoR9YH4qTrL3jx69Ekf7ww70Qea5enLE8xUgQfGTOaEHxkFcEovmzv54 +6IVe083iK8alXD/9OUTaDY9NwMnOn1K1aU2XOfliGGLgwwaHg+wVFh5rZIHsDl7v +=Embu +-----END PGP PUBLIC KEY BLOCK----- +` + // MaxRetries specifies how often non-fatally failing HTTP requests should be retried. MaxRetries = 4 // MaxRequestDuration defines the maximum amount of time that a request and its retries may take in total diff --git a/repositories/gcs.go b/repositories/gcs.go index 8f3f404a..04a306f1 100644 --- a/repositories/gcs.go +++ b/repositories/gcs.go @@ -22,60 +22,6 @@ const ( ltsBaseURL = "https://releases.bazel.build" commitBaseURL = "https://storage.googleapis.com/bazel-builds/artifacts" lastGreenCommitURL = "https://storage.googleapis.com/bazel-builds/last_green_commit/github.com/bazelbuild/bazel.git/publish-bazel-binaries" - verificationKey = ` ------BEGIN PGP PUBLIC KEY BLOCK----- - -mQINBFdEmzkBEACzj8tMYUau9oFZWNDytcQWazEO6LrTTtdQ98d3JcnVyrpT16yg -I/QfGXA8LuDdKYpUDNjehLtBL3IZp4xe375Jh8v2IA2iQ5RXGN+lgKJ6rNwm15Kr -qYeCZlU9uQVpZuhKLXsWK6PleyQHjslNUN/HtykIlmMz4Nnl3orT7lMI5rsGCmk0 -1Kth0DFh8SD9Vn2G4huddwxM8/tYj1QmWPCTgybATNuZ0L60INH8v6+J2jJzViVc -NRnR7mpouGmRy/rcr6eY9QieOwDou116TrVRFfcBRhocCI5b6uCRuhaqZ6Qs28Bx -4t5JVksXJ7fJoTy2B2s/rPx/8j4MDVEdU8b686ZDHbKYjaYBYEfBqePXScp8ndul -XWwS2lcedPihOUl6oQQYy59inWIpxi0agm0MXJAF1Bc3ToSQdHw/p0Y21kYxE2pg -EaUeElVccec5poAaHSPprUeej9bD9oIC4sMCsLs7eCQx2iP+cR7CItz6GQtuZrvS -PnKju1SKl5iwzfDQGpi6u6UAMFmc53EaH05naYDAigCueZ+/2rIaY358bECK6/VR -kyrBqpeq6VkWUeOkt03VqoPzrw4gEzRvfRtLj+D2j/pZCH3vyMYHzbaaXBv6AT0e -RmgtGo9I9BYqKSWlGEF0D+CQ3uZfOyovvrbYqNaHynFBtrx/ZkM82gMA5QARAQAB -tEdCYXplbCBEZXZlbG9wZXIgKEJhemVsIEFQVCByZXBvc2l0b3J5IGtleSkgPGJh -emVsLWRldkBnb29nbGVncm91cHMuY29tPokCVQQTAQgAPwIbAwYLCQgHAwIGFQgC -CQoLBBYCAwECHgECF4AWIQRxodDvz+tigf0EN8k9WRm0SEV+4AUCXsoWGgUJC0fh -4QAKCRA9WRm0SEV+4NDCD/9c5rhZREBlikdi5QYRq1YOkwzJLXFoVe0FonEwMuWK -fQzT/rIwyh14tssptU5+eXwTEXL0ZDskgzvrFSpzjQZzcSG/gzNCATNfrZpC2nfE -SxMKOeIwQedn26YIHCI8s9tEQ7BSvfBfJgqfIo3IURhmfzNMj+qszca+3IDYAlAy -8lxUVbJcIQ0apnAdnIadtydzca56mMN7ma+btddaWLpAdyfUvQ/Zsx3TYYLF7inQ -km0JpzISN0fGngzGNDGNmtHNhCdSpyfkr+7fvpbKAYkSH7uZ1AIPDyHdLIwDQnX2 -kbLRkxKncKGSDhUSdlJTl0x36cU+xmgO15FFdOyk3BUfrlfDrgXIBjeX8KNh9TV6 -HgFFR/mNONoJ93ZvZQNO2s1gbPZJe3VJ1Q5PMLW1sdl8q8JthBwT/5TJ1k8E5VYj -jAc8dl+RAALxqj+eo5xI45o1FdV5s1aGDjbwFoCIhGCy2zaog1q5wnhmEptAAD0S -TVbJSpwNiLlPIcGVaCjXp8Ow3SzOGTRKIjFTO/I6FiSJOpgfri07clXmnb4ETjou -mUdglg8/8nQ120zHEOqoSzzIbTNUDjNZY8SuY6Ig3/ObQ/JAFS0i6h74KLfXUZzn -uETY7KURLdyPAhL37Hb9FDhvkJCUO/l6eqDh9jk1JjB7Cvb7hEvnbvDrr2hWNAL7 -RrkCDQRXRJs5ARAA55/1VBlDpV/ElUyLmRyPCz/V+msHdinyw4Mv5DJQupuZwlMy -vxPPzc7GmsIfk1zuOzDWirNs22r43ak6dsAvpcU+iVBi46MqUcbNtC+kfxlKiToD -PCs82rdfCgHT7XYDzrCWlqNQ9++BqM2OYRIxyEucizeofWPlrJUgKvu8fWLVZ6bY -n4L/PqAhobhuSjRcoB5Tp81hGa4cscKIGIqhymfnguaY8viJ83tHPUqQJoApNPy8 -q1pWHSDV6zBv71beqV2b6cBzp7VqNYOIuqE6ZNBFWuCG3zRc9ia2/bHxx2TGAQJt -PpPzitm0xkB3GGN06YnnSCE+f2j+7F0IO6uFlSy7ho0PoSFbDgR91kJK3S0ZBZx4 -H21cIpWWBzf9Nd1M4H3O7KhnGSZDq6+tXZ9/F/ZUvCZHpQlJewDPY9315Ymacf5C -Zk8xeE5UUIxFMdOxF8B7Itb6rbFWv+tzWdX/0/M8/b0ZJhVvngWzuh/agdS4E5an -f7ahGWM96jPRIQEb9DRN2YGp9hOiX2sZqkhxE5zWqD2gdXp2ZAxMCTHf4ijzOVsO -nde7b5BqC0JL73gNwf1iOHyCAzqGiFfah8/odBTDhMsdVMsjSIxzcwlwRnzy+hBs -dYpP19ieJCMoERJTbUgSspPdhY/Y4ChzlFHjiAKYT6vXiYcKS04stCtHqwEAEQEA -AYkCPAQYAQgAJgIbDBYhBHGh0O/P62KB/QQ3yT1ZGbRIRX7gBQJeyhYlBQkLR+Hs -AAoJED1ZGbRIRX7g3Y8P/iuOAHmyCMeSELvUs9ZvLYJKGzmz67R8fJSmgst/Bs3p -dWCAjGE56M6UgZzHXK+fBRWFPDOXT64XNq0UIG7tThthwe4Gdvg/5rWG61Pe/vCZ -2FkMAlEMkuufZYMcw9jItHMKLcYyW/jtN9EzCX+vM6SZlu4o8la5rCIBEaiKfzft -a/dRMjW+RqQnU31NQCDAy3zoGUCQumJtv3GVbMYHIrRZua2yyNo9Iborh2SVdBbK -v9WJKH4JcCHd0/XDGdys6EXeATIIRxchumkmxpIg87OhsC0n5yuH1FnFIFQEjbYX -bb46F7ZFT+8Tov+lgMEw4CZmps4uvvZlKbIH4Zi/ULiobwvm2ad3nejWICmGmHYz -ro6t08hdcY6GnOzCpDwx9yHechMCkU3KEE98nb/CxcmA4VzDHudTJe7o0OyaSarh -6D5WcXf7D9FfcKmUD9xaCsfXh66OCksMVGE1JctrO1wQTF2jTdTUq7mmi30tlM+o -JjVk65OSOd4JYol8auzE4oXOfsNzXbyvj7WzM1v5m7C45jOL+Ly7I3IUzZNfF41J -AMmSd73EOoR9YH4qTrL3jx69Ekf7ww70Qea5enLE8xUgQfGTOaEHxkFcEovmzv54 -6IVe083iK8alXD/9OUTaDY9NwMnOn1K1aU2XOfliGGLgwwaHg+wVFh5rZIHsDl7v -=Embu ------END PGP PUBLIC KEY BLOCK----- -` ) // GCSRepo represents a Bazel repository on Google Cloud Storage that contains Bazel releases, release candidates and Bazel binaries built at arbitrary commits. @@ -250,7 +196,7 @@ func (gcs *GCSRepo) DownloadLTS(version, destDir, destFile string, config config } url := fmt.Sprintf("%s/%s/%s/%s", ltsBaseURL, baseVersion, folder, srcFile) - return httputil.DownloadBinary(url, url+".sig", verificationKey, destDir, destFile, config) + return httputil.DownloadBinary(url, url+".sig", httputil.VerificationKey, destDir, destFile, config) } // CommitRepo diff --git a/repositories/github.go b/repositories/github.go index 9d8a7b87..763817e6 100644 --- a/repositories/github.go +++ b/repositories/github.go @@ -91,5 +91,5 @@ func (gh *GitHubRepo) DownloadVersion(fork, version, destDir, destFile string, c return "", err } url := fmt.Sprintf(urlPattern, fork, version, filename) - return httputil.DownloadBinary(url, "", "", destDir, destFile, config) + return httputil.DownloadBinary(url, url+".sig", httputil.VerificationKey, destDir, destFile, config) } From 10b77a7fd608aa016b689e2b7533ae85b0f4bef0 Mon Sep 17 00:00:00 2001 From: Valentin Grigorev Date: Wed, 21 Jan 2026 23:45:40 +0100 Subject: [PATCH 03/13] Support authentication for downloading signatures --- httputil/httputil.go | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/httputil/httputil.go b/httputil/httputil.go index 0a11df8f..b685fe71 100644 --- a/httputil/httputil.go +++ b/httputil/httputil.go @@ -245,6 +245,20 @@ func tryFindNetrcFileCreds(host string) (string, error) { return fmt.Sprintf("Basic %s", token), nil } +func getAuthForURL(rawURL string) (string, error) { + u, err := url.Parse(rawURL) + if err != nil { + // rawURL is supposed to be valid + return "", err + } + + t, err := tryFindNetrcFileCreds(u.Host) + if err != nil { + return "", nil + } + return t, nil +} + // DownloadBinary downloads a file from the given URL into the specified location, marks it executable and returns its full path. func DownloadBinary(originURL, signatureURL, verificationKey, destDir, destFile string, config config.Config) (string, error) { err := os.MkdirAll(destDir, 0755) @@ -265,21 +279,12 @@ func DownloadBinary(originURL, signatureURL, verificationKey, destDir, destFile } }() - u, err := url.Parse(originURL) + auth, err := getAuthForURL(originURL) if err != nil { - // originURL supposed to be valid return "", err } log.Printf("Downloading %s...", originURL) - - var auth string = "" - t, err := tryFindNetrcFileCreds(u.Host) - if err == nil { - // successfully parsed netrc for given host - auth = t - } - resp, err := get(originURL, auth) if err != nil { return "", fmt.Errorf("HTTP GET %s failed: %w", originURL, err) @@ -307,7 +312,13 @@ func DownloadBinary(originURL, signatureURL, verificationKey, destDir, destFile } if signatureURL != "" && verificationKey != "" { - signature, err := get(signatureURL, "") + signatureAuth, err := getAuthForURL(signatureURL) + if err != nil { + return "", err + } + + log.Printf("Downloading %s...", signatureURL) + signature, err := get(signatureURL, signatureAuth) if err != nil { return "", fmt.Errorf("HTTP GET %s failed: %v", signatureURL, err) } From 8c14b8e3849b8843cdc8191080d562bdc31f9914 Mon Sep 17 00:00:00 2001 From: Valentin Grigorev Date: Fri, 23 Jan 2026 12:42:23 +0100 Subject: [PATCH 04/13] Rework Bazel downloading and verification: * refactor `httputil.DownloadBinary` to download and store signature file * extract authenticity verification logic into a separate `VerifyBinary` function * perform authenticity verification in `downloadBazelIfNecessary`, after integrity check * it allows us to keep verification logic in one place * failure of authenticity check is clearly handled in the same way as failure of integrity check: downloaded Bazel left in CAS, but the mapping file is not created in metadata --- core/core.go | 81 +++++++++++++++----- core/repositories.go | 54 +++++++------- httputil/httputil.go | 165 ++++++++++++++++++++++++----------------- repositories/gcs.go | 18 ++--- repositories/github.go | 6 +- 5 files changed, 198 insertions(+), 126 deletions(-) diff --git a/core/core.go b/core/core.go index 7cfc0ed7..23bba0e2 100644 --- a/core/core.go +++ b/core/core.go @@ -457,11 +457,13 @@ func downloadBazelIfNecessary(version string, bazeliskHome string, bazelForkOrUR } } - pathToBazelInCAS, downloadedDigest, err := downloadBazelToCAS(version, bazeliskHome, repos, config, downloader) + artifact, downloadedDigest, err := downloadBazelToCAS(version, bazeliskHome, repos, config, downloader) if err != nil { return "", fmt.Errorf("failed to download bazel: %w", err) } + pathToBazelInCAS, pathToSignatureInCAS := artifact.BinaryPath, artifact.SignaturePath + // Verifying integrity of downloaded binary (if it was requested) expectedSha256 := strings.ToLower(config.Get("BAZELISK_VERIFY_SHA256")) if len(expectedSha256) > 0 { if expectedSha256 != downloadedDigest { @@ -469,6 +471,12 @@ func downloadBazelIfNecessary(version string, bazeliskHome string, bazelForkOrUR } } + // Verifying authenticity of downloaded binary (if it was requested) + if err := verifyBinaryAuthenticity(pathToBazelInCAS, pathToSignatureInCAS); err != nil { + return "", err + } + + // Verification is finished successfully, write the mapping file if err := atomicWriteFile(mappingPath, []byte(downloadedDigest), 0644); err != nil { return "", fmt.Errorf("failed to write mapping file after downloading bazel: %w", err) } @@ -476,6 +484,31 @@ func downloadBazelIfNecessary(version string, bazeliskHome string, bazelForkOrUR return pathToBazelInCAS, nil } +func verifyBinaryAuthenticity(binaryPath, signaturePath string) error { + binary, err := os.Open(binaryPath) + if err != nil { + return fmt.Errorf("could not open binary %s for verification: %v", binaryPath, err) + } + defer binary.Close() + + signature, err := os.Open(signaturePath) + if err != nil { + return fmt.Errorf("could not open signature %s for verification: %v", signaturePath, err) + } + defer signature.Close() + + entity, err := httputil.VerifyBinary(binary, signature) + if err != nil { + return fmt.Errorf("failed to verify authenticity of downloaded file %s using detached signature from %s and embedded verification key: %v", binaryPath, signaturePath, err) + } + + for _, identity := range entity.Identities { + log.Printf("Signed by %s", identity.Name) + } + + return nil +} + func atomicWriteFile(path string, contents []byte, perm os.FileMode) error { parent := filepath.Dir(path) if err := os.MkdirAll(parent, 0755); err != nil { @@ -525,43 +558,48 @@ func lockedRenameIfDstAbsent(src, dst string) error { return os.Rename(src, dst) } -func downloadBazelToCAS(version string, bazeliskHome string, repos *Repositories, config config.Config, downloader DownloadFunc) (string, string, error) { +func downloadBazelToCAS(version string, bazeliskHome string, repos *Repositories, config config.Config, downloader DownloadFunc) (httputil.DownloadArtifact, string, error) { downloadsDir := filepath.Join(bazeliskHome, "downloads") temporaryDownloadDir := filepath.Join(downloadsDir, "_tmp") casDir := filepath.Join(bazeliskHome, "downloads", "sha256") tmpDestFileBytes := make([]byte, 32) if _, err := rand.Read(tmpDestFileBytes); err != nil { - return "", "", fmt.Errorf("failed to generate temporary file name: %w", err) + return httputil.DownloadArtifact{}, "", fmt.Errorf("failed to generate temporary file name: %w", err) } tmpDestFile := fmt.Sprintf("%x", tmpDestFileBytes) - var tmpDestPath string + var artifact httputil.DownloadArtifact var err error baseURL := config.Get(BaseURLEnv) formatURL := config.Get(FormatURLEnv) + if baseURL != "" && formatURL != "" { - return "", "", fmt.Errorf("cannot set %s and %s at once", BaseURLEnv, FormatURLEnv) + return httputil.DownloadArtifact{}, "", fmt.Errorf("cannot set %s and %s at once", BaseURLEnv, FormatURLEnv) } else if formatURL != "" { - tmpDestPath, err = repos.DownloadFromFormatURL(config, formatURL, version, temporaryDownloadDir, tmpDestFile) + artifact, err = repos.DownloadFromFormatURL(config, formatURL, version, temporaryDownloadDir, tmpDestFile) } else if baseURL != "" { - tmpDestPath, err = repos.DownloadFromBaseURL(baseURL, version, temporaryDownloadDir, tmpDestFile, config) + artifact, err = repos.DownloadFromBaseURL(baseURL, version, temporaryDownloadDir, tmpDestFile, config) } else { - tmpDestPath, err = downloader(temporaryDownloadDir, tmpDestFile) + artifact, err = downloader(temporaryDownloadDir, tmpDestFile) } + if err != nil { - return "", "", fmt.Errorf("failed to download bazel: %w", err) + return artifact, "", fmt.Errorf("failed to download bazel: %w", err) } + tmpDestPath := artifact.BinaryPath + tmpSignaturePath := artifact.SignaturePath + f, err := os.Open(tmpDestPath) if err != nil { - return "", "", fmt.Errorf("failed to open downloaded bazel to digest it: %w", err) + return artifact, "", fmt.Errorf("failed to open downloaded bazel to digest it: %w", err) } h := sha256.New() if _, err := io.Copy(h, f); err != nil { f.Close() - return "", "", fmt.Errorf("cannot compute sha256 of %s after download: %v", tmpDestPath, err) + return artifact, "", fmt.Errorf("cannot compute sha256 of %s after download: %v", tmpDestPath, err) } f.Close() actualSha256 := strings.ToLower(fmt.Sprintf("%x", h.Sum(nil))) @@ -570,24 +608,32 @@ func downloadBazelToCAS(version string, bazeliskHome string, repos *Repositories pathToBazelInCAS := filepath.Join(casDir, actualSha256, "bin", bazelInCASBasename) dirForBazelInCAS := filepath.Dir(pathToBazelInCAS) if err := os.MkdirAll(dirForBazelInCAS, 0755); err != nil { - return "", "", fmt.Errorf("failed to MkdirAll parent of %s: %w", pathToBazelInCAS, err) + return artifact, "", fmt.Errorf("failed to MkdirAll parent of %s: %w", pathToBazelInCAS, err) } tmpPathFile, err := os.CreateTemp(dirForBazelInCAS, bazelInCASBasename+".tmp") if err != nil { - return "", "", fmt.Errorf("failed to create temporary file in %s: %w", dirForBazelInCAS, err) + return artifact, "", fmt.Errorf("failed to create temporary file in %s: %w", dirForBazelInCAS, err) } tmpPathFile.Close() defer os.Remove(tmpPathFile.Name()) tmpPathInCorrectDirectory := tmpPathFile.Name() if err := os.Rename(tmpDestPath, tmpPathInCorrectDirectory); err != nil { - return "", "", fmt.Errorf("failed to move %s to %s: %w", tmpDestPath, tmpPathInCorrectDirectory, err) + return artifact, "", fmt.Errorf("failed to move %s to %s: %w", tmpDestPath, tmpPathInCorrectDirectory, err) } if err := lockedRenameIfDstAbsent(tmpPathInCorrectDirectory, pathToBazelInCAS); err != nil { - return "", "", fmt.Errorf("failed to move %s to %s: %w", tmpPathInCorrectDirectory, pathToBazelInCAS, err) + return artifact, "", fmt.Errorf("failed to move %s to %s: %w", tmpPathInCorrectDirectory, pathToBazelInCAS, err) + } + + if tmpSignaturePath == "" { //TODO: if signature was requested, it's an error to not have it here! + return httputil.DownloadArtifact{}, "", fmt.Errorf("signature file for %s was requested but not received", tmpDestPath) + } + pathToSignatureInCAS := pathToBazelInCAS + ".sig" + if err := lockedRenameIfDstAbsent(tmpSignaturePath, pathToSignatureInCAS); err != nil { + return httputil.DownloadArtifact{}, "", fmt.Errorf("failed to move signature file %s to %s: %w", tmpSignaturePath, pathToSignatureInCAS, err) } - return pathToBazelInCAS, actualSha256, nil + return httputil.DownloadArtifact{BinaryPath: pathToBazelInCAS, SignaturePath: pathToSignatureInCAS}, actualSha256, nil } func copyFile(src, dst string, perm os.FileMode) error { @@ -1395,10 +1441,11 @@ func downloadInstallerToCAS(installerURL, bazeliskHome string, config config.Con tmpInstallerFile := fmt.Sprintf("%x-installer", tmpInstallerBytes) // Download the installer - installerPath, err := httputil.DownloadBinary(installerURL, installerURL+".sig", httputil.VerificationKey, temporaryDownloadDir, tmpInstallerFile, config) + artifact, err := httputil.DownloadBinary(installerURL, installerURL+".sig", temporaryDownloadDir, tmpInstallerFile, config) if err != nil { return "", fmt.Errorf("failed to download installer: %w", err) } + installerPath := artifact.BinaryPath defer os.Remove(installerPath) // Read installer content and compute hash diff --git a/core/repositories.go b/core/repositories.go index 1c1c6bb0..43442e6e 100644 --- a/core/repositories.go +++ b/core/repositories.go @@ -20,7 +20,7 @@ const ( ) // DownloadFunc downloads a specific Bazel binary to the given location and returns the absolute path. -type DownloadFunc func(destDir, destFile string) (string, error) +type DownloadFunc func(destDir, destFile string) (httputil.DownloadArtifact, error) // LTSFilter filters Bazel versions based on specific criteria. type LTSFilter func(string) bool @@ -39,7 +39,7 @@ type LTSRepo interface { GetLTSVersions(bazeliskHome string, opts *FilterOpts) ([]string, error) // DownloadLTS downloads the given Bazel version into the specified location and returns the absolute path. - DownloadLTS(version, destDir, destFile string, config config.Config) (string, error) + DownloadLTS(version, destDir, destFile string, config config.Config) (httputil.DownloadArtifact, error) } // ForkRepo represents a repository that stores a fork of Bazel (releases). @@ -48,7 +48,7 @@ type ForkRepo interface { GetVersions(bazeliskHome, fork string) ([]string, error) // DownloadVersion downloads the given Bazel binary from the specified fork into the given location and returns the absolute path. - DownloadVersion(fork, version, destDir, destFile string, config config.Config) (string, error) + DownloadVersion(fork, version, destDir, destFile string, config config.Config) (httputil.DownloadArtifact, error) } // CommitRepo represents a repository that stores Bazel binaries built at specific commits. @@ -58,7 +58,7 @@ type CommitRepo interface { GetLastGreenCommit(bazeliskHome string) (string, error) // DownloadAtCommit downloads a Bazel binary built at the given commit into the specified location and returns the absolute path. - DownloadAtCommit(commit, destDir, destFile string, config config.Config) (string, error) + DownloadAtCommit(commit, destDir, destFile string, config config.Config) (httputil.DownloadArtifact, error) } // RollingRepo represents a repository that stores rolling Bazel releases. @@ -67,7 +67,7 @@ type RollingRepo interface { GetRollingVersions(bazeliskHome string) ([]string, error) // DownloadRolling downloads the given Bazel version into the specified location and returns the absolute path. - DownloadRolling(version, destDir, destFile string, config config.Config) (string, error) + DownloadRolling(version, destDir, destFile string, config config.Config) (httputil.DownloadArtifact, error) } // Repositories offers access to different types of Bazel repositories, mainly for finding and downloading the correct version of Bazel. @@ -110,7 +110,7 @@ func (r *Repositories) resolveFork(bazeliskHome string, vi *versions.Info, confi if err != nil { return "", nil, err } - downloader := func(destDir, destFile string) (string, error) { + downloader := func(destDir, destFile string) (httputil.DownloadArtifact, error) { return r.Fork.DownloadVersion(vi.Fork, version, destDir, destFile, config) } return version, downloader, nil @@ -149,7 +149,7 @@ func (r *Repositories) resolveLTS(bazeliskHome string, vi *versions.Info, config if err != nil { return "", nil, err } - downloader := func(destDir, destFile string) (string, error) { + downloader := func(destDir, destFile string) (httputil.DownloadArtifact, error) { return r.LTS.DownloadLTS(version, destDir, destFile, config) } return version, downloader, nil @@ -164,7 +164,7 @@ func (r *Repositories) resolveCommit(bazeliskHome string, vi *versions.Info, con return "", nil, fmt.Errorf("cannot resolve last green commit: %v", err) } } - downloader := func(destDir, destFile string) (string, error) { + downloader := func(destDir, destFile string) (httputil.DownloadArtifact, error) { return r.Commits.DownloadAtCommit(version, destDir, destFile, config) } return version, downloader, nil @@ -178,7 +178,7 @@ func (r *Repositories) resolveRolling(bazeliskHome string, vi *versions.Info, co if err != nil { return "", nil, err } - downloader := func(destDir, destFile string) (string, error) { + downloader := func(destDir, destFile string) (httputil.DownloadArtifact, error) { return r.Rolling.DownloadRolling(version, destDir, destFile, config) } return version, downloader, nil @@ -205,21 +205,21 @@ func resolvePotentiallyRelativeVersion(bazeliskHome string, lister listVersionsF } // DownloadFromBaseURL can download Bazel binaries from a specific URL while ignoring the predefined repositories. -func (r *Repositories) DownloadFromBaseURL(baseURL, version, destDir, destFile string, config config.Config) (string, error) { +func (r *Repositories) DownloadFromBaseURL(baseURL, version, destDir, destFile string, config config.Config) (httputil.DownloadArtifact, error) { if !r.supportsBaseOrFormatURL { - return "", fmt.Errorf("downloads from %s are forbidden", BaseURLEnv) + return httputil.DownloadArtifact{}, fmt.Errorf("downloads from %s are forbidden", BaseURLEnv) } if baseURL == "" { - return "", fmt.Errorf("%s is not set", BaseURLEnv) + return httputil.DownloadArtifact{}, fmt.Errorf("%s is not set", BaseURLEnv) } srcFile, err := platforms.DetermineBazelFilename(version, true, config) if err != nil { - return "", err + return httputil.DownloadArtifact{}, err } url := fmt.Sprintf("%s/%s/%s", baseURL, version, srcFile) - return httputil.DownloadBinary(url, url+".sig", httputil.VerificationKey, destDir, destFile, config) + return httputil.DownloadBinary(url, url+".sig", destDir, destFile, config) } // BuildURLFromFormat returns a Bazel download URL based on formatURL. @@ -269,20 +269,20 @@ func BuildURLFromFormat(config config.Config, formatURL, version string) (string } // DownloadFromFormatURL can download Bazel binaries from a specific URL while ignoring the predefined repositories. -func (r *Repositories) DownloadFromFormatURL(config config.Config, formatURL, version, destDir, destFile string) (string, error) { +func (r *Repositories) DownloadFromFormatURL(config config.Config, formatURL, version, destDir, destFile string) (httputil.DownloadArtifact, error) { if !r.supportsBaseOrFormatURL { - return "", fmt.Errorf("downloads from %s are forbidden", FormatURLEnv) + return httputil.DownloadArtifact{}, fmt.Errorf("downloads from %s are forbidden", FormatURLEnv) } if formatURL == "" { - return "", fmt.Errorf("%s is not set", FormatURLEnv) + return httputil.DownloadArtifact{}, fmt.Errorf("%s is not set", FormatURLEnv) } url, err := BuildURLFromFormat(config, formatURL, version) if err != nil { - return "", err + return httputil.DownloadArtifact{}, err } - return httputil.DownloadBinary(url, url+".sig", httputil.VerificationKey, destDir, destFile, config) + return httputil.DownloadBinary(url, url+".sig", destDir, destFile, config) } // CreateRepositories creates a new Repositories instance with the given repositories. Any nil repository will be replaced by a dummy repository that raises an error whenever a download is attempted. @@ -327,8 +327,8 @@ func (nolts *noLTSRepo) GetLTSVersions(bazeliskHome string, opts *FilterOpts) ([ return nil, nolts.err } -func (nolts *noLTSRepo) DownloadLTS(version, destDir, destFile string, config config.Config) (string, error) { - return "", nolts.err +func (nolts *noLTSRepo) DownloadLTS(version, destDir, destFile string, config config.Config) (httputil.DownloadArtifact, error) { + return httputil.DownloadArtifact{}, nolts.err } type noForkRepo struct { @@ -339,8 +339,8 @@ func (nfr *noForkRepo) GetVersions(bazeliskHome, fork string) ([]string, error) return nil, nfr.err } -func (nfr *noForkRepo) DownloadVersion(fork, version, destDir, destFile string, config config.Config) (string, error) { - return "", nfr.err +func (nfr *noForkRepo) DownloadVersion(fork, version, destDir, destFile string, config config.Config) (httputil.DownloadArtifact, error) { + return httputil.DownloadArtifact{}, nfr.err } type noCommitRepo struct { @@ -351,8 +351,8 @@ func (nlgr *noCommitRepo) GetLastGreenCommit(bazeliskHome string) (string, error return "", nlgr.err } -func (nlgr *noCommitRepo) DownloadAtCommit(commit, destDir, destFile string, config config.Config) (string, error) { - return "", nlgr.err +func (nlgr *noCommitRepo) DownloadAtCommit(commit, destDir, destFile string, config config.Config) (httputil.DownloadArtifact, error) { + return httputil.DownloadArtifact{}, nlgr.err } type noRollingRepo struct { @@ -363,6 +363,6 @@ func (nrr *noRollingRepo) GetRollingVersions(bazeliskHome string) ([]string, err return nil, nrr.err } -func (nrr *noRollingRepo) DownloadRolling(version, destDir, destFile string, config config.Config) (string, error) { - return "", nrr.err +func (nrr *noRollingRepo) DownloadRolling(version, destDir, destFile string, config config.Config) (httputil.DownloadArtifact, error) { + return httputil.DownloadArtifact{}, nrr.err } diff --git a/httputil/httputil.go b/httputil/httputil.go index b685fe71..3aac6e28 100644 --- a/httputil/httputil.go +++ b/httputil/httputil.go @@ -5,7 +5,6 @@ import ( b64 "encoding/base64" "errors" "fmt" - "golang.org/x/crypto/openpgp" "io" "log" "math/rand" @@ -18,6 +17,8 @@ import ( "strings" "time" + "golang.org/x/crypto/openpgp" + netrc "github.com/bgentry/go-netrc/netrc" homedir "github.com/mitchellh/go-homedir" @@ -259,104 +260,128 @@ func getAuthForURL(rawURL string) (string, error) { return t, nil } +type DownloadArtifact struct { + BinaryPath string + SignaturePath string +} + +func DownloadFile(url string, destFile *os.File, config config.Config) error { + auth, err := getAuthForURL(url) + if err != nil { + return err + } + + log.Printf("Downloading %s...", url) + resp, err := get(url, auth) + if err != nil { + return fmt.Errorf("HTTP GET %s failed: %w", url, err) + } + defer resp.Body.Close() + + if resp.StatusCode == 404 { + return NotFound + } else if resp.StatusCode != 200 { + return fmt.Errorf("HTTP GET %s failed with error %v", url, resp.StatusCode) + } + + _, err = io.Copy( + // Add a progress bar during download. + progress.Writer(destFile, "Downloading", resp.ContentLength, config), + resp.Body) + progress.Finish(config) + if err != nil { + return fmt.Errorf("could not copy from %s to %s: %v", url, destFile.Name(), err) + } + + return nil +} + +func createTempFile(destDir, pattern string) (*os.File, func(), error) { + tmpFile, err := os.CreateTemp(destDir, pattern) + if err != nil { + return nil, nil, fmt.Errorf("could not create temporary file: %v", err) + } + return tmpFile, func() { + err := tmpFile.Close() + if err == nil { + os.Remove(tmpFile.Name()) + } + }, nil +} + +func VerifyBinary(signedBinary, signature io.Reader) (*openpgp.Entity, error) { + keys, err := openpgp.ReadArmoredKeyRing(strings.NewReader(VerificationKey)) + if err != nil { + return nil, fmt.Errorf("failed to load the embedded Verification Key") + } + if len(keys) != 1 { + return nil, fmt.Errorf("failed to load the embedded Verification Key") + } + + entity, err := openpgp.CheckDetachedSignature(keys, signedBinary, signature) + if err != nil { + return nil, err + } + + return entity, nil +} + // DownloadBinary downloads a file from the given URL into the specified location, marks it executable and returns its full path. -func DownloadBinary(originURL, signatureURL, verificationKey, destDir, destFile string, config config.Config) (string, error) { +func DownloadBinary(originURL, signatureURL, destDir, destFile string, config config.Config) (DownloadArtifact, error) { err := os.MkdirAll(destDir, 0755) if err != nil { - return "", fmt.Errorf("could not create directory %s: %v", destDir, err) + return DownloadArtifact{}, fmt.Errorf("could not create directory %s: %v", destDir, err) } destinationPath := filepath.Join(destDir, destFile) + destinationSignaturePath := destinationPath + ".sig" if _, err := os.Stat(destinationPath); err != nil { - tmpfile, err := os.CreateTemp(destDir, "download") - if err != nil { - return "", fmt.Errorf("could not create temporary file: %v", err) - } - defer func() { - err := tmpfile.Close() - if err == nil { - os.Remove(tmpfile.Name()) - } - }() - - auth, err := getAuthForURL(originURL) + originTmpFile, originCleanFunc, err := createTempFile(destDir, "download") if err != nil { - return "", err + return DownloadArtifact{}, fmt.Errorf("could not create temporary file: %v", err) } + defer originCleanFunc() - log.Printf("Downloading %s...", originURL) - resp, err := get(originURL, auth) + err = DownloadFile(originURL, originTmpFile, config) if err != nil { - return "", fmt.Errorf("HTTP GET %s failed: %w", originURL, err) + return DownloadArtifact{}, fmt.Errorf("failed to download %s: %v", originURL, err) } - defer resp.Body.Close() - - if resp.StatusCode == 404 { - return "", NotFound - } else if resp.StatusCode != 200 { - return "", fmt.Errorf("HTTP GET %s failed with error %v", originURL, resp.StatusCode) - } - - _, err = io.Copy( - // Add a progress bar during download. - progress.Writer(tmpfile, "Downloading", resp.ContentLength, config), - resp.Body) - progress.Finish(config) + err = os.Chmod(originTmpFile.Name(), 0755) if err != nil { - return "", fmt.Errorf("could not copy from %s to %s: %v", originURL, tmpfile.Name(), err) + return DownloadArtifact{}, fmt.Errorf("could not chmod file %s: %v", originTmpFile.Name(), err) } - err = os.Chmod(tmpfile.Name(), 0755) - if err != nil { - return "", fmt.Errorf("could not chmod file %s: %v", tmpfile.Name(), err) - } - - if signatureURL != "" && verificationKey != "" { - signatureAuth, err := getAuthForURL(signatureURL) + if signatureURL != "" { //todo: and signature verification is requested + signatureTmpFile, signatureCleanFunc, err := createTempFile(destDir, "download-signature-") if err != nil { - return "", err + return DownloadArtifact{}, fmt.Errorf("could not create temporary file: %v", err) } + defer signatureCleanFunc() - log.Printf("Downloading %s...", signatureURL) - signature, err := get(signatureURL, signatureAuth) + err = DownloadFile(signatureURL, signatureTmpFile, config) if err != nil { - return "", fmt.Errorf("HTTP GET %s failed: %v", signatureURL, err) - } - defer signature.Body.Close() - - if signature.StatusCode != 200 { - return "", fmt.Errorf("HTTP GET %s failed with error %v", signatureURL, signature.StatusCode) + return DownloadArtifact{}, fmt.Errorf("failed to download %s: %v", signatureURL, err) } - keys, err := openpgp.ReadArmoredKeyRing(strings.NewReader(verificationKey)) + signatureTmpFile.Close() + err = os.Rename(signatureTmpFile.Name(), destinationSignaturePath) if err != nil { - return "", fmt.Errorf("failed to load the embedded Verification Key") - } - - if len(keys) != 1 { - return "", fmt.Errorf("failed to load the embedded Verification Key") - } - - tmpfile.Seek(0, io.SeekStart) - - entity, err := openpgp.CheckDetachedSignature(keys, tmpfile, signature.Body) - if err != nil { - return "", fmt.Errorf("failed to verify the downloaded file using signature from %s", signatureURL) - } - - for _, identity := range entity.Identities { - log.Printf("Signed by %s", identity.Name) + return DownloadArtifact{}, fmt.Errorf("could not move %s to %s: %v", signatureTmpFile.Name(), destinationSignaturePath, err) } } - tmpfile.Close() - err = os.Rename(tmpfile.Name(), destinationPath) + originTmpFile.Close() + err = os.Rename(originTmpFile.Name(), destinationPath) if err != nil { - return "", fmt.Errorf("could not move %s to %s: %v", tmpfile.Name(), destinationPath, err) + return DownloadArtifact{}, fmt.Errorf("could not move %s to %s: %v", originTmpFile.Name(), destinationPath, err) } } + //todo: check that signature file exists if signature verification is requested! + if _, err := os.Stat(destinationSignaturePath); err != nil { + return DownloadArtifact{}, fmt.Errorf("%s already exists, but corresponding signature file %s does not exist or unaccessable: %v", destinationPath, destinationSignaturePath, err) + } - return destinationPath, nil + return DownloadArtifact{destinationPath, destinationSignaturePath}, nil } // ContentMerger is a function that merges multiple HTTP payloads into a single message. diff --git a/repositories/gcs.go b/repositories/gcs.go index 04a306f1..484a5acd 100644 --- a/repositories/gcs.go +++ b/repositories/gcs.go @@ -181,10 +181,10 @@ func getTrack(version string) (int, error) { } // DownloadLTS downloads the given Bazel LTS release (candidate) into the specified location and returns the absolute path. -func (gcs *GCSRepo) DownloadLTS(version, destDir, destFile string, config config.Config) (string, error) { +func (gcs *GCSRepo) DownloadLTS(version, destDir, destFile string, config config.Config) (httputil.DownloadArtifact, error) { srcFile, err := platforms.DetermineBazelFilename(version, true, config) if err != nil { - return "", err + return httputil.DownloadArtifact{}, err } var baseVersion, folder string @@ -196,7 +196,7 @@ func (gcs *GCSRepo) DownloadLTS(version, destDir, destFile string, config config } url := fmt.Sprintf("%s/%s/%s/%s", ltsBaseURL, baseVersion, folder, srcFile) - return httputil.DownloadBinary(url, url+".sig", httputil.VerificationKey, destDir, destFile, config) + return httputil.DownloadBinary(url, url+".sig", destDir, destFile, config) } // CommitRepo @@ -218,14 +218,14 @@ func (gcs *GCSRepo) GetLastGreenCommit(bazeliskHome string) (string, error) { } // DownloadAtCommit downloads a Bazel binary built at the given commit into the specified location and returns the absolute path. -func (gcs *GCSRepo) DownloadAtCommit(commit, destDir, destFile string, config config.Config) (string, error) { +func (gcs *GCSRepo) DownloadAtCommit(commit, destDir, destFile string, config config.Config) (httputil.DownloadArtifact, error) { log.Printf("Using unreleased version at commit %s", commit) platform, err := platforms.GetPlatform() if err != nil { - return "", err + return httputil.DownloadArtifact{}, err } url := fmt.Sprintf("%s/%s/%s/bazel", commitBaseURL, platform, commit) - return httputil.DownloadBinary(url, "", "", destDir, destFile, config) + return httputil.DownloadBinary(url, "", destDir, destFile, config) } // RollingRepo @@ -266,13 +266,13 @@ func (gcs *GCSRepo) GetRollingVersions(bazeliskHome string) ([]string, error) { } // DownloadRolling downloads the given Bazel version into the specified location and returns the absolute path. -func (gcs *GCSRepo) DownloadRolling(version, destDir, destFile string, config config.Config) (string, error) { +func (gcs *GCSRepo) DownloadRolling(version, destDir, destFile string, config config.Config) (httputil.DownloadArtifact, error) { srcFile, err := platforms.DetermineBazelFilename(version, true, config) if err != nil { - return "", err + return httputil.DownloadArtifact{}, err } releaseVersion := strings.Split(version, "-")[0] url := fmt.Sprintf("%s/%s/rolling/%s/%s", ltsBaseURL, releaseVersion, version, srcFile) - return httputil.DownloadBinary(url, "", "", destDir, destFile, config) + return httputil.DownloadBinary(url, "", destDir, destFile, config) } diff --git a/repositories/github.go b/repositories/github.go index 763817e6..6a3e2aeb 100644 --- a/repositories/github.go +++ b/repositories/github.go @@ -85,11 +85,11 @@ type gitHubRelease struct { } // DownloadVersion downloads a Bazel binary for the given version and fork to the specified location and returns the absolute path. -func (gh *GitHubRepo) DownloadVersion(fork, version, destDir, destFile string, config config.Config) (string, error) { +func (gh *GitHubRepo) DownloadVersion(fork, version, destDir, destFile string, config config.Config) (httputil.DownloadArtifact, error) { filename, err := platforms.DetermineBazelFilename(version, true, config) if err != nil { - return "", err + return httputil.DownloadArtifact{}, err } url := fmt.Sprintf(urlPattern, fork, version, filename) - return httputil.DownloadBinary(url, url+".sig", httputil.VerificationKey, destDir, destFile, config) + return httputil.DownloadBinary(url, url+".sig", destDir, destFile, config) } From ffee135a04eeef3a70a5b97fbf86402ae96a41ea Mon Sep 17 00:00:00 2001 From: Valentin Grigorev Date: Sat, 24 Jan 2026 20:13:55 +0100 Subject: [PATCH 05/13] Introduce BAZELISK_NO_SIGNATURE_VERIFICATION config value to allow disabling of authenticity check --- README.md | 3 +++ core/core.go | 8 ++++++-- httputil/httputil.go | 11 ++++++----- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index e11549da..3b5792d2 100644 --- a/README.md +++ b/README.md @@ -241,6 +241,8 @@ This behavior can be disabled by setting the environment variable `BAZELISK_SKIP You can control the user agent that Bazelisk sends in all HTTP requests by setting `BAZELISK_USER_AGENT` to the desired value. +You can disable the authenticity check of downloaded Bazel binaries by setting the environment variable `BAZELISK_NO_SIGNATURE_VERIFICATION` to any value (except the empty string) before launching Bazelisk. + # .bazeliskrc configuration file A `.bazeliskrc` file in the root directory of a workspace or the user home directory allows users to set environment variables persistently. (The Python implementation of Bazelisk doesn't check the user home directory yet, only the workspace directory.) @@ -265,6 +267,7 @@ The following variables can be set: - `BAZELISK_HOME_WINDOWS` - `BAZELISK_HOME` - `BAZELISK_INCOMPATIBLE_FLAGS` +- `BAZELISK_NO_SIGNATURE_VERIFICATION` - `BAZELISK_SHOW_PROGRESS` - `BAZELISK_SHUTDOWN` - `BAZELISK_SKIP_WRAPPER` diff --git a/core/core.go b/core/core.go index 23bba0e2..bb56beff 100644 --- a/core/core.go +++ b/core/core.go @@ -472,8 +472,12 @@ func downloadBazelIfNecessary(version string, bazeliskHome string, bazelForkOrUR } // Verifying authenticity of downloaded binary (if it was requested) - if err := verifyBinaryAuthenticity(pathToBazelInCAS, pathToSignatureInCAS); err != nil { - return "", err + if config.Get("BAZELISK_NO_SIGNATURE_VERIFICATION") == "" { + if err := verifyBinaryAuthenticity(pathToBazelInCAS, pathToSignatureInCAS); err != nil { + return "", err + } + } else { + log.Printf("Skipping signature verification because BAZELISK_NO_SIGNATURE_VERIFICATION is set.") } // Verification is finished successfully, write the mapping file diff --git a/httputil/httputil.go b/httputil/httputil.go index 3aac6e28..ef645d3e 100644 --- a/httputil/httputil.go +++ b/httputil/httputil.go @@ -351,7 +351,8 @@ func DownloadBinary(originURL, signatureURL, destDir, destFile string, config co return DownloadArtifact{}, fmt.Errorf("could not chmod file %s: %v", originTmpFile.Name(), err) } - if signatureURL != "" { //todo: and signature verification is requested + // download the signature file if signature verification is requested + if config.Get("BAZELISK_NO_SIGNATURE_VERIFICATION") == "" && signatureURL != "" { signatureTmpFile, signatureCleanFunc, err := createTempFile(destDir, "download-signature-") if err != nil { return DownloadArtifact{}, fmt.Errorf("could not create temporary file: %v", err) @@ -375,10 +376,10 @@ func DownloadBinary(originURL, signatureURL, destDir, destFile string, config co if err != nil { return DownloadArtifact{}, fmt.Errorf("could not move %s to %s: %v", originTmpFile.Name(), destinationPath, err) } - } - //todo: check that signature file exists if signature verification is requested! - if _, err := os.Stat(destinationSignaturePath); err != nil { - return DownloadArtifact{}, fmt.Errorf("%s already exists, but corresponding signature file %s does not exist or unaccessable: %v", destinationPath, destinationSignaturePath, err) + } else if config.Get("BAZELISK_NO_SIGNATURE_VERIFICATION") == "" { + if _, err := os.Stat(destinationSignaturePath); err != nil { + return DownloadArtifact{}, fmt.Errorf("%s already exists, but corresponding signature file %s does not exist or unaccessable: %v", destinationPath, destinationSignaturePath, err) + } } return DownloadArtifact{destinationPath, destinationSignaturePath}, nil From 5e12585f8e964d1d966f897c54fb9f906c435edc Mon Sep 17 00:00:00 2001 From: Valentin Grigorev Date: Sat, 24 Jan 2026 23:10:03 +0100 Subject: [PATCH 06/13] Introduce BAZELISK_VERIFICATION_KEY_FILE config value to allow explicitly using an alternative verification key. It can be useful if * the embedded verification key expired, but it's impossible to update bazelisk for some reason * Bazel is downloaded from the fork which uses an alternative PGP key --- README.md | 3 +++ core/core.go | 35 ++++++++++++++++++++++++++--------- httputil/httputil.go | 11 ++++------- 3 files changed, 33 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 3b5792d2..d009bad5 100644 --- a/README.md +++ b/README.md @@ -243,6 +243,8 @@ You can control the user agent that Bazelisk sends in all HTTP requests by setti You can disable the authenticity check of downloaded Bazel binaries by setting the environment variable `BAZELISK_NO_SIGNATURE_VERIFICATION` to any value (except the empty string) before launching Bazelisk. +You can provide an alternative PGP public key for binary authenticity verification by setting `BAZELISK_VERIFICATION_KEY_FILE` to the path of the key file. + # .bazeliskrc configuration file A `.bazeliskrc` file in the root directory of a workspace or the user home directory allows users to set environment variables persistently. (The Python implementation of Bazelisk doesn't check the user home directory yet, only the workspace directory.) @@ -272,6 +274,7 @@ The following variables can be set: - `BAZELISK_SHUTDOWN` - `BAZELISK_SKIP_WRAPPER` - `BAZELISK_USER_AGENT` +- `BAZELISK_VERIFICATION_KEY_FILE` - `BAZELISK_VERIFY_SHA256` - `USE_BAZEL_VERSION` diff --git a/core/core.go b/core/core.go index bb56beff..7b544be3 100644 --- a/core/core.go +++ b/core/core.go @@ -472,12 +472,8 @@ func downloadBazelIfNecessary(version string, bazeliskHome string, bazelForkOrUR } // Verifying authenticity of downloaded binary (if it was requested) - if config.Get("BAZELISK_NO_SIGNATURE_VERIFICATION") == "" { - if err := verifyBinaryAuthenticity(pathToBazelInCAS, pathToSignatureInCAS); err != nil { - return "", err - } - } else { - log.Printf("Skipping signature verification because BAZELISK_NO_SIGNATURE_VERIFICATION is set.") + if err := verifyBinaryAuthenticity(pathToBazelInCAS, pathToSignatureInCAS, config); err != nil { + return "", err } // Verification is finished successfully, write the mapping file @@ -488,7 +484,12 @@ func downloadBazelIfNecessary(version string, bazeliskHome string, bazelForkOrUR return pathToBazelInCAS, nil } -func verifyBinaryAuthenticity(binaryPath, signaturePath string) error { +func verifyBinaryAuthenticity(binaryPath, signaturePath string, config config.Config) error { + if config.Get("BAZELISK_NO_SIGNATURE_VERIFICATION") != "" { + log.Printf("Skipping signature verification because BAZELISK_NO_SIGNATURE_VERIFICATION is set.") + return nil + } + binary, err := os.Open(binaryPath) if err != nil { return fmt.Errorf("could not open binary %s for verification: %v", binaryPath, err) @@ -501,9 +502,25 @@ func verifyBinaryAuthenticity(binaryPath, signaturePath string) error { } defer signature.Close() - entity, err := httputil.VerifyBinary(binary, signature) + var verificationKey string + var verificationKeySource string + + verificationKeyPath := config.Get("BAZELISK_VERIFICATION_KEY_FILE") + if verificationKeyPath != "" { + data, err := os.ReadFile(verificationKeyPath) + if err != nil { + return fmt.Errorf("failed to read verification key from %s: %v", verificationKeyPath, err) + } + verificationKey = string(data) + verificationKeySource = fmt.Sprintf("verification key from %s", verificationKeyPath) + } else { + verificationKey = httputil.VerificationKey + verificationKeySource = "embedded verification key" + } + + entity, err := httputil.VerifyBinary(binary, signature, verificationKey) if err != nil { - return fmt.Errorf("failed to verify authenticity of downloaded file %s using detached signature from %s and embedded verification key: %v", binaryPath, signaturePath, err) + return fmt.Errorf("failed to verify authenticity of downloaded file %s using detached signature from %s and %s: %v", binaryPath, signaturePath, verificationKeySource, err) } for _, identity := range entity.Identities { diff --git a/httputil/httputil.go b/httputil/httputil.go index ef645d3e..0c47fb25 100644 --- a/httputil/httputil.go +++ b/httputil/httputil.go @@ -309,13 +309,10 @@ func createTempFile(destDir, pattern string) (*os.File, func(), error) { }, nil } -func VerifyBinary(signedBinary, signature io.Reader) (*openpgp.Entity, error) { - keys, err := openpgp.ReadArmoredKeyRing(strings.NewReader(VerificationKey)) - if err != nil { - return nil, fmt.Errorf("failed to load the embedded Verification Key") - } - if len(keys) != 1 { - return nil, fmt.Errorf("failed to load the embedded Verification Key") +func VerifyBinary(signedBinary, signature io.Reader, verificationKey string) (*openpgp.Entity, error) { + keys, err := openpgp.ReadArmoredKeyRing(strings.NewReader(verificationKey)) + if err != nil || len(keys) != 1 { + return nil, fmt.Errorf("failed to load the verification Key") } entity, err := openpgp.CheckDetachedSignature(keys, signedBinary, signature) From d3b82751e15f5af48fe93d774be627697aecd517 Mon Sep 17 00:00:00 2001 From: Valentin Grigorev Date: Sun, 25 Jan 2026 14:04:49 +0100 Subject: [PATCH 07/13] Switch from golang.org/x/crypto to github.com/ProtonMail/gopenpgp/v3, because golang.org/x/crypto is deprecated and unmaintained See https://pkg.go.dev/golang.org/x/crypto/openpgp for details --- MODULE.bazel | 2 +- core/core.go | 18 +++++++++++------- go.mod | 9 +++++++-- go.sum | 6 ++++++ httputil/BUILD | 2 +- httputil/httputil.go | 35 ++++++++++++++++++++++++++--------- 6 files changed, 52 insertions(+), 20 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index 373e9624..8eb8f645 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -27,6 +27,6 @@ use_repo( "com_github_gofrs_flock", "com_github_hashicorp_go_version", "com_github_mitchellh_go_homedir", - "org_golang_x_crypto", + "com_github_protonmail_gopenpgp_v3", "org_golang_x_term", ) diff --git a/core/core.go b/core/core.go index 7b544be3..fab3a20b 100644 --- a/core/core.go +++ b/core/core.go @@ -503,7 +503,7 @@ func verifyBinaryAuthenticity(binaryPath, signaturePath string, config config.Co defer signature.Close() var verificationKey string - var verificationKeySource string + //var verificationKeySource string verificationKeyPath := config.Get("BAZELISK_VERIFICATION_KEY_FILE") if verificationKeyPath != "" { @@ -512,19 +512,23 @@ func verifyBinaryAuthenticity(binaryPath, signaturePath string, config config.Co return fmt.Errorf("failed to read verification key from %s: %v", verificationKeyPath, err) } verificationKey = string(data) - verificationKeySource = fmt.Sprintf("verification key from %s", verificationKeyPath) + //verificationKeySource = fmt.Sprintf("verification key from %s", verificationKeyPath) } else { verificationKey = httputil.VerificationKey - verificationKeySource = "embedded verification key" + //verificationKeySource = "embedded verification key" } - entity, err := httputil.VerifyBinary(binary, signature, verificationKey) + verificationResult, err := httputil.VerifyBinary(binary, signature, verificationKey) if err != nil { - return fmt.Errorf("failed to verify authenticity of downloaded file %s using detached signature from %s and %s: %v", binaryPath, signaturePath, verificationKeySource, err) + return err + } + + if err = verificationResult.SignatureError(); err != nil { + return verificationResult.SignatureError() } - for _, identity := range entity.Identities { - log.Printf("Signed by %s", identity.Name) + for identity := range verificationResult.SignedByKey().GetEntity().Identities { + log.Printf("Signed by \"%s\"", identity) } return nil diff --git a/go.mod b/go.mod index 81ad1819..166cf781 100644 --- a/go.mod +++ b/go.mod @@ -5,12 +5,17 @@ go 1.24.0 toolchain go1.24.2 require ( + github.com/ProtonMail/gopenpgp/v3 v3.3.0 github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d github.com/gofrs/flock v0.13.0 github.com/hashicorp/go-version v1.7.0 github.com/mitchellh/go-homedir v1.1.0 golang.org/x/term v0.39.0 - golang.org/x/crypto v0.45.0 ) -require golang.org/x/sys v0.40.0 // indirect +require ( + github.com/ProtonMail/go-crypto v1.3.0 // indirect + github.com/cloudflare/circl v1.6.1 // indirect + golang.org/x/crypto v0.45.0 // indirect + golang.org/x/sys v0.40.0 // indirect +) diff --git a/go.sum b/go.sum index c86a3234..61662a0f 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,11 @@ +github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= +github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= +github.com/ProtonMail/gopenpgp/v3 v3.3.0 h1:N6rHCH5PWwB6zSRMgRj1EbAMQHUAAHxH3Oo4KibsPwY= +github.com/ProtonMail/gopenpgp/v3 v3.3.0/go.mod h1:J+iNPt0/5EO9wRt7Eit9dRUlzyu3hiGX3zId6iuaKOk= github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= +github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= +github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= diff --git a/httputil/BUILD b/httputil/BUILD index 9e365968..a9de1b13 100644 --- a/httputil/BUILD +++ b/httputil/BUILD @@ -17,7 +17,7 @@ go_library( "//httputil/progress", "@com_github_bgentry_go_netrc//netrc", "@com_github_mitchellh_go_homedir//:go-homedir", - "@org_golang_x_crypto//openpgp" + "@com_github_protonmail_gopenpgp_v3//crypto", ], ) diff --git a/httputil/httputil.go b/httputil/httputil.go index 0c47fb25..a3427812 100644 --- a/httputil/httputil.go +++ b/httputil/httputil.go @@ -14,10 +14,9 @@ import ( "path/filepath" "regexp" "strconv" - "strings" "time" - "golang.org/x/crypto/openpgp" + "github.com/ProtonMail/gopenpgp/v3/crypto" netrc "github.com/bgentry/go-netrc/netrc" homedir "github.com/mitchellh/go-homedir" @@ -309,18 +308,36 @@ func createTempFile(destDir, pattern string) (*os.File, func(), error) { }, nil } -func VerifyBinary(signedBinary, signature io.Reader, verificationKey string) (*openpgp.Entity, error) { - keys, err := openpgp.ReadArmoredKeyRing(strings.NewReader(verificationKey)) - if err != nil || len(keys) != 1 { - return nil, fmt.Errorf("failed to load the verification Key") +func VerifyBinary(binary, signature io.Reader, verificationKey string) (*crypto.VerifyResult, error) { + pgp := crypto.PGP() + key, err := crypto.NewKeyFromArmored(verificationKey) + if err != nil { + return nil, fmt.Errorf("failed to load the embedded Verification Key: %v", err) + } + + keys, err := crypto.NewKeyRing(key) + if err != nil { + return nil, fmt.Errorf("failed to create keyring: %v", err) + } + + verifier, err := pgp.Verify(). + VerificationKeys(keys). + New() + if err != nil { + return nil, fmt.Errorf("failed to create verifier: %v", err) + } + + verifyDataReader, err := verifier.VerifyingReader(binary, signature, crypto.Auto) + if err != nil { + return nil, fmt.Errorf("failed to create verifying reader: %v", err) } - entity, err := openpgp.CheckDetachedSignature(keys, signedBinary, signature) + result, err := verifyDataReader.DiscardAllAndVerifySignature() if err != nil { - return nil, err + return nil, fmt.Errorf("failed to verify authenticity of downloaded file: %v", err) } - return entity, nil + return result, nil } // DownloadBinary downloads a file from the given URL into the specified location, marks it executable and returns its full path. From b391e0de42bb0acef6461e1d7ba445cc205e1fd9 Mon Sep 17 00:00:00 2001 From: Valentin Grigorev Date: Mon, 26 Jan 2026 14:57:33 +0100 Subject: [PATCH 08/13] Verify authenticity for rolling releases --- repositories/gcs.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repositories/gcs.go b/repositories/gcs.go index 484a5acd..1ccf99f4 100644 --- a/repositories/gcs.go +++ b/repositories/gcs.go @@ -274,5 +274,5 @@ func (gcs *GCSRepo) DownloadRolling(version, destDir, destFile string, config co releaseVersion := strings.Split(version, "-")[0] url := fmt.Sprintf("%s/%s/rolling/%s/%s", ltsBaseURL, releaseVersion, version, srcFile) - return httputil.DownloadBinary(url, "", destDir, destFile, config) + return httputil.DownloadBinary(url, url+".sig", destDir, destFile, config) } From 1711bfe4355a0e9c1b097751fcce6143812b6450 Mon Sep 17 00:00:00 2001 From: Valentin Grigorev Date: Mon, 26 Jan 2026 13:45:10 +0100 Subject: [PATCH 09/13] Don't try to verify authenticity when no signature is available (unreleased versions) --- core/core.go | 17 +++++++++++------ httputil/httputil.go | 4 ++++ repositories/gcs.go | 5 +++++ 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/core/core.go b/core/core.go index fab3a20b..19d130d7 100644 --- a/core/core.go +++ b/core/core.go @@ -650,12 +650,17 @@ func downloadBazelToCAS(version string, bazeliskHome string, repos *Repositories return artifact, "", fmt.Errorf("failed to move %s to %s: %w", tmpPathInCorrectDirectory, pathToBazelInCAS, err) } - if tmpSignaturePath == "" { //TODO: if signature was requested, it's an error to not have it here! - return httputil.DownloadArtifact{}, "", fmt.Errorf("signature file for %s was requested but not received", tmpDestPath) - } - pathToSignatureInCAS := pathToBazelInCAS + ".sig" - if err := lockedRenameIfDstAbsent(tmpSignaturePath, pathToSignatureInCAS); err != nil { - return httputil.DownloadArtifact{}, "", fmt.Errorf("failed to move signature file %s to %s: %w", tmpSignaturePath, pathToSignatureInCAS, err) + var pathToSignatureInCAS string + if config.Get("BAZELISK_NO_SIGNATURE_VERIFICATION") == "" { + if tmpSignaturePath == "" { + return httputil.DownloadArtifact{}, "", fmt.Errorf("signature file for %s was requested but not received", tmpDestPath) + } + pathToSignatureInCAS = pathToBazelInCAS + ".sig" + if err := lockedRenameIfDstAbsent(tmpSignaturePath, pathToSignatureInCAS); err != nil { + return httputil.DownloadArtifact{}, "", fmt.Errorf("failed to move signature file %s to %s: %w", tmpSignaturePath, pathToSignatureInCAS, err) + } + } else { + pathToSignatureInCAS = "" } return httputil.DownloadArtifact{BinaryPath: pathToBazelInCAS, SignaturePath: pathToSignatureInCAS}, actualSha256, nil diff --git a/httputil/httputil.go b/httputil/httputil.go index a3427812..d9db0a7a 100644 --- a/httputil/httputil.go +++ b/httputil/httputil.go @@ -349,6 +349,10 @@ func DownloadBinary(originURL, signatureURL, destDir, destFile string, config co destinationPath := filepath.Join(destDir, destFile) destinationSignaturePath := destinationPath + ".sig" + if signatureURL == "" && config.Get("BAZELISK_NO_SIGNATURE_VERIFICATION") == "" { + return DownloadArtifact{}, fmt.Errorf("signature verification is requested, but no signature URL was provided") + } + if _, err := os.Stat(destinationPath); err != nil { originTmpFile, originCleanFunc, err := createTempFile(destDir, "download") if err != nil { diff --git a/repositories/gcs.go b/repositories/gcs.go index 1ccf99f4..b3e0ca44 100644 --- a/repositories/gcs.go +++ b/repositories/gcs.go @@ -7,6 +7,7 @@ import ( "encoding/json" "fmt" "log" + "os" "strconv" "strings" "time" @@ -225,6 +226,10 @@ func (gcs *GCSRepo) DownloadAtCommit(commit, destDir, destFile string, config co return httputil.DownloadArtifact{}, err } url := fmt.Sprintf("%s/%s/%s/bazel", commitBaseURL, platform, commit) + + log.Printf("No signature is available for unreleased version at commit %s, forcefully setting BAZELISK_NO_SIGNATURE_VERIFICATION=1", commit) + os.Setenv("BAZELISK_NO_SIGNATURE_VERIFICATION", "1") + return httputil.DownloadBinary(url, "", destDir, destFile, config) } From da5cd469907be0dd4ca922a106740e51ec66dd7b Mon Sep 17 00:00:00 2001 From: Valentin Grigorev Date: Mon, 26 Jan 2026 01:37:21 +0100 Subject: [PATCH 10/13] Handle expired verification key especially --- MODULE.bazel | 1 + core/BUILD | 1 + core/core.go | 22 ++++++++++++++++++---- go.mod | 2 +- 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index 8eb8f645..30c690c7 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -27,6 +27,7 @@ use_repo( "com_github_gofrs_flock", "com_github_hashicorp_go_version", "com_github_mitchellh_go_homedir", + "com_github_protonmail_go_crypto", "com_github_protonmail_gopenpgp_v3", "org_golang_x_term", ) diff --git a/core/BUILD b/core/BUILD index d265c285..ac23d85c 100644 --- a/core/BUILD +++ b/core/BUILD @@ -19,6 +19,7 @@ go_library( "//ws", "@com_github_gofrs_flock//:flock", "@com_github_mitchellh_go_homedir//:go-homedir", + "@com_github_protonmail_go_crypto//openpgp/errors", ], ) diff --git a/core/core.go b/core/core.go index 19d130d7..f196e33c 100644 --- a/core/core.go +++ b/core/core.go @@ -34,6 +34,8 @@ import ( "github.com/bazelbuild/bazelisk/ws" "github.com/gofrs/flock" "github.com/mitchellh/go-homedir" + + pgpErrors "github.com/ProtonMail/go-crypto/openpgp/errors" ) const ( @@ -503,7 +505,7 @@ func verifyBinaryAuthenticity(binaryPath, signaturePath string, config config.Co defer signature.Close() var verificationKey string - //var verificationKeySource string + var verificationKeySource string verificationKeyPath := config.Get("BAZELISK_VERIFICATION_KEY_FILE") if verificationKeyPath != "" { @@ -512,10 +514,10 @@ func verifyBinaryAuthenticity(binaryPath, signaturePath string, config config.Co return fmt.Errorf("failed to read verification key from %s: %v", verificationKeyPath, err) } verificationKey = string(data) - //verificationKeySource = fmt.Sprintf("verification key from %s", verificationKeyPath) + verificationKeySource = fmt.Sprintf("Verification key from %s", verificationKeyPath) } else { verificationKey = httputil.VerificationKey - //verificationKeySource = "embedded verification key" + verificationKeySource = "Embedded verification key" } verificationResult, err := httputil.VerifyBinary(binary, signature, verificationKey) @@ -524,7 +526,19 @@ func verifyBinaryAuthenticity(binaryPath, signaturePath string, config config.Co } if err = verificationResult.SignatureError(); err != nil { - return verificationResult.SignatureError() + if errors.Is(err, pgpErrors.ErrKeyExpired) { + var msgStart string + if verificationKeyPath == "" { + msgStart = "Either update bazelisk to a newer version or use" + } else { + msgStart = "Use" + } + return fmt.Errorf("%s is expired!\n"+ + "%s BAZELISK_VERIFICATION_KEY_FILE to set an alternative verification key externally.\n"+ + "Up to date verification key should be available at https://bazel.build/bazel-release.pub.gpg.", + verificationKeySource, msgStart) + } + return err } for identity := range verificationResult.SignedByKey().GetEntity().Identities { diff --git a/go.mod b/go.mod index 166cf781..b74dca03 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.24.0 toolchain go1.24.2 require ( + github.com/ProtonMail/go-crypto v1.3.0 github.com/ProtonMail/gopenpgp/v3 v3.3.0 github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d github.com/gofrs/flock v0.13.0 @@ -14,7 +15,6 @@ require ( ) require ( - github.com/ProtonMail/go-crypto v1.3.0 // indirect github.com/cloudflare/circl v1.6.1 // indirect golang.org/x/crypto v0.45.0 // indirect golang.org/x/sys v0.40.0 // indirect From 268b037f718be92ae0d1eeeb6041c31ffe854d41 Mon Sep 17 00:00:00 2001 From: Valentin Grigorev Date: Mon, 26 Jan 2026 01:59:03 +0100 Subject: [PATCH 11/13] Update embedded verification key --- httputil/httputil.go | 98 +++++++++++++++++++++++++++----------------- 1 file changed, 61 insertions(+), 37 deletions(-) diff --git a/httputil/httputil.go b/httputil/httputil.go index d9db0a7a..26666466 100644 --- a/httputil/httputil.go +++ b/httputil/httputil.go @@ -51,43 +51,67 @@ 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 -=Embu +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 +MZaF+SBEZXd1ZSqB5Q== +=laPs -----END PGP PUBLIC KEY BLOCK----- ` From 20e31e07f9b162ee4e9bea7bb1c72d3d3ee39b5b Mon Sep 17 00:00:00 2001 From: Valentin Grigorev Date: Mon, 26 Jan 2026 12:36:47 +0100 Subject: [PATCH 12/13] Add tests for authenticity verification logic --- core/BUILD | 1 + core/core_test.go | 81 +++++++++++++++++++ httputil/BUILD | 7 +- httputil/httputil_test.go | 48 +++++++++++ httputil/httputil_test_helper/BUILD | 14 ++++ .../httputil_test_helper.go | 54 +++++++++++++ 6 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 httputil/httputil_test_helper/BUILD create mode 100644 httputil/httputil_test_helper/httputil_test_helper.go diff --git a/core/BUILD b/core/BUILD index ac23d85c..3591e94c 100644 --- a/core/BUILD +++ b/core/BUILD @@ -32,6 +32,7 @@ go_test( embed = [":core"], deps = [ "//config", + "//httputil/httputil_test_helper", "//platforms", ], ) diff --git a/core/core_test.go b/core/core_test.go index 567be609..948564e2 100644 --- a/core/core_test.go +++ b/core/core_test.go @@ -14,6 +14,7 @@ import ( "testing" "github.com/bazelbuild/bazelisk/config" + "github.com/bazelbuild/bazelisk/httputil/httputil_test_helper" "github.com/bazelbuild/bazelisk/platforms" ) @@ -888,3 +889,83 @@ func TestRunBazeliskWithStderrRedirection(t *testing.T) { t.Error("stdout content should not appear in stderr") } } + +func TestVerifyBinaryAuthenticity(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "TestVerifyBinaryAuthenticity") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + key, err := httputil_test_helper.GenerateTestKey("Bazelisk Test", "test@bazel.build") + if err != nil { + t.Fatalf("Failed to generate test key: %v", err) + } + + binaryPath := filepath.Join(tmpDir, "bazel") + content := []byte("binary content") + if err := os.WriteFile(binaryPath, content, 0644); err != nil { + t.Fatalf("Failed to write binary: %v", err) + } + + signature, err := httputil_test_helper.SignMessage(content, key) + if err != nil { + t.Fatalf("Failed to sign message: %v", err) + } + signaturePath := binaryPath + ".sig" + if err := os.WriteFile(signaturePath, []byte(signature), 0644); err != nil { + t.Fatalf("Failed to write signature: %v", err) + } + + keyPath := filepath.Join(tmpDir, "key.pub") + if err := os.WriteFile(keyPath, []byte(key), 0644); err != nil { + t.Fatalf("Failed to write key: %v", err) + } + + t.Run("ValidSignatureWithKeyFile", func(t *testing.T) { + cfg := config.Static(map[string]string{ + "BAZELISK_VERIFICATION_KEY_FILE": keyPath, + }) + err := verifyBinaryAuthenticity(binaryPath, signaturePath, cfg) + if err != nil { + t.Errorf("verifyBinaryAuthenticity failed: %v", err) + } + }) + + t.Run("NoSignatureVerification", func(t *testing.T) { + cfg := config.Static(map[string]string{ + "BAZELISK_NO_SIGNATURE_VERIFICATION": "1", + }) + // Use invalid signature path, should still pass because verification is skipped + err := verifyBinaryAuthenticity(binaryPath, "nonexistent.sig", cfg) + if err != nil { + t.Errorf("verifyBinaryAuthenticity should have skipped verification: %v", err) + } + }) + + t.Run("InvalidSignature", func(t *testing.T) { + cfg := config.Static(map[string]string{ + "BAZELISK_VERIFICATION_KEY_FILE": keyPath, + }) + wrongContent := []byte("wrong content") + wrongBinaryPath := filepath.Join(tmpDir, "bazel_wrong") + if err := os.WriteFile(wrongBinaryPath, wrongContent, 0644); err != nil { + t.Fatalf("Failed to write wrong binary: %v", err) + } + + err := verifyBinaryAuthenticity(wrongBinaryPath, signaturePath, cfg) + if err == nil { + t.Error("Expected error for invalid signature, but got none") + } + }) + + t.Run("MissingKeyFile", func(t *testing.T) { + cfg := config.Static(map[string]string{ + "BAZELISK_VERIFICATION_KEY_FILE": "nonexistent.key", + }) + err := verifyBinaryAuthenticity(binaryPath, signaturePath, cfg) + if err == nil { + t.Error("Expected error for missing key file, but got none") + } + }) +} diff --git a/httputil/BUILD b/httputil/BUILD index a9de1b13..7e4d4a78 100644 --- a/httputil/BUILD +++ b/httputil/BUILD @@ -23,6 +23,11 @@ go_library( go_test( name = "httputil_test", - srcs = ["httputil_test.go"], + srcs = [ + "httputil_test.go", + ], embed = [":httputil"], + deps = [ + "//httputil/httputil_test_helper", + ], ) diff --git a/httputil/httputil_test.go b/httputil/httputil_test.go index 1a3f2281..c4d92736 100644 --- a/httputil/httputil_test.go +++ b/httputil/httputil_test.go @@ -7,6 +7,8 @@ import ( "strings" "testing" "time" + + "github.com/bazelbuild/bazelisk/httputil/httputil_test_helper" ) var ( @@ -251,3 +253,49 @@ func TestNoRetryOnPermanentError(t *testing.T) { t.Fatalf("Expected no retries for permanent error, but got %d", clock.TimesSlept()) } } + +func TestVerifyBinary(t *testing.T) { + key, err := httputil_test_helper.GenerateTestKey("Bazelisk Test", "test@bazel.build") + if err != nil { + t.Fatalf("Failed to generate test key: %v", err) + } + + content := []byte("important content") + signature, err := httputil_test_helper.SignMessage(content, key) + if err != nil { + t.Fatalf("Failed to sign message: %v", err) + } + + t.Run("ValidSignature", func(t *testing.T) { + res, err := VerifyBinary(strings.NewReader(string(content)), strings.NewReader(signature), key) + if err != nil { + t.Fatalf("VerifyBinary failed: %v", err) + } + if err := res.SignatureError(); err != nil { + t.Fatalf("Signature error: %v", err) + } + }) + + t.Run("InvalidSignature", func(t *testing.T) { + res, err := VerifyBinary(strings.NewReader("different content"), strings.NewReader(signature), key) + if err != nil { + // VerifyBinary might return an error or a result with a signature error + return + } + if err := res.SignatureError(); err == nil { + t.Fatal("Expected signature error for different content, but got none") + } + }) + + t.Run("InvalidKey", func(t *testing.T) { + otherKey, _ := httputil_test_helper.GenerateTestKey("Other Key", "other@example.com") + _, err := VerifyBinary(strings.NewReader(string(content)), strings.NewReader(signature), otherKey) + if err == nil { + // In some cases it might return a result with error instead of error + // but usually failing to find the key in keyring for signature is an error or sig error + } + }) +} + +// It would be nice to have a test for an expired key too, but it occurred to be too complicated and +// not expressible in terms of GopenPGP V3. diff --git a/httputil/httputil_test_helper/BUILD b/httputil/httputil_test_helper/BUILD new file mode 100644 index 00000000..197150d0 --- /dev/null +++ b/httputil/httputil_test_helper/BUILD @@ -0,0 +1,14 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "httputil_test_helper", + testonly = True, + srcs = [ + "httputil_test_helper.go", + ], + importpath = "github.com/bazelbuild/bazelisk/httputil/httputil_test_helper", + visibility = ["//visibility:public"], + deps = [ + "@com_github_protonmail_gopenpgp_v3//crypto", + ], +) diff --git a/httputil/httputil_test_helper/httputil_test_helper.go b/httputil/httputil_test_helper/httputil_test_helper.go new file mode 100644 index 00000000..981b02c4 --- /dev/null +++ b/httputil/httputil_test_helper/httputil_test_helper.go @@ -0,0 +1,54 @@ +package httputil_test_helper + +import ( + "github.com/ProtonMail/gopenpgp/v3/crypto" +) + +func GenerateTestKey(name, email string) (string, error) { + pgp := crypto.PGP() + handle := pgp.KeyGeneration(). + AddUserId(name, email). + New() + key, err := handle.GenerateKey() + if err != nil { + return "", err + } + return key.Armor() +} + +func SignMessage(message []byte, armoredKey string) (string, error) { + pgp := crypto.PGP() + key, err := crypto.NewKeyFromArmored(armoredKey) + if err != nil { + return "", err + } + keyring, err := crypto.NewKeyRing(key) + if err != nil { + return "", err + } + signer, err := pgp.Sign(). + SigningKeys(keyring). + Detached(). + New() + if err != nil { + return "", err + } + signature, err := signer.Sign(message, crypto.Armor) + if err != nil { + return "", err + } + return string(signature), nil +} + +func GetExpiredTestKey(name, email string) (string, error) { + pgp := crypto.PGP() + handle := pgp.KeyGeneration(). + AddUserId(name, email). + Lifetime(1). + New() + key, err := handle.GenerateKey() + if err != nil { + return "", err + } + return key.Armor() +} From bb6e9ad32a1664f03c04314f4cd8f9b70d27f58c Mon Sep 17 00:00:00 2001 From: Valentin Grigorev Date: Tue, 27 Jan 2026 15:17:29 +0100 Subject: [PATCH 13/13] Store verification key in a separate file bazel_key.pub.gpg and use go:embed to make it available in the source code --- httputil/BUILD | 3 ++ httputil/bazel_key.pub.gpg | 76 ++++++++++++++++++++++++++++++++++ httputil/httputil.go | 84 ++------------------------------------ 3 files changed, 83 insertions(+), 80 deletions(-) create mode 100644 httputil/bazel_key.pub.gpg diff --git a/httputil/BUILD b/httputil/BUILD index 7e4d4a78..a3a11f26 100644 --- a/httputil/BUILD +++ b/httputil/BUILD @@ -10,6 +10,9 @@ go_library( "fake.go", "httputil.go", ], + embedsrcs = [ + "bazel_key.pub.gpg", + ], importpath = "github.com/bazelbuild/bazelisk/httputil", visibility = ["//visibility:public"], deps = [ diff --git a/httputil/bazel_key.pub.gpg b/httputil/bazel_key.pub.gpg new file mode 100644 index 00000000..a89049b8 --- /dev/null +++ b/httputil/bazel_key.pub.gpg @@ -0,0 +1,76 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBFdEmzkBEACzj8tMYUau9oFZWNDytcQWazEO6LrTTtdQ98d3JcnVyrpT16yg +I/QfGXA8LuDdKYpUDNjehLtBL3IZp4xe375Jh8v2IA2iQ5RXGN+lgKJ6rNwm15Kr +qYeCZlU9uQVpZuhKLXsWK6PleyQHjslNUN/HtykIlmMz4Nnl3orT7lMI5rsGCmk0 +1Kth0DFh8SD9Vn2G4huddwxM8/tYj1QmWPCTgybATNuZ0L60INH8v6+J2jJzViVc +NRnR7mpouGmRy/rcr6eY9QieOwDou116TrVRFfcBRhocCI5b6uCRuhaqZ6Qs28Bx +4t5JVksXJ7fJoTy2B2s/rPx/8j4MDVEdU8b686ZDHbKYjaYBYEfBqePXScp8ndul +XWwS2lcedPihOUl6oQQYy59inWIpxi0agm0MXJAF1Bc3ToSQdHw/p0Y21kYxE2pg +EaUeElVccec5poAaHSPprUeej9bD9oIC4sMCsLs7eCQx2iP+cR7CItz6GQtuZrvS +PnKju1SKl5iwzfDQGpi6u6UAMFmc53EaH05naYDAigCueZ+/2rIaY358bECK6/VR +kyrBqpeq6VkWUeOkt03VqoPzrw4gEzRvfRtLj+D2j/pZCH3vyMYHzbaaXBv6AT0e +RmgtGo9I9BYqKSWlGEF0D+CQ3uZfOyovvrbYqNaHynFBtrx/ZkM82gMA5QARAQAB +tEdCYXplbCBEZXZlbG9wZXIgKEJhemVsIEFQVCByZXBvc2l0b3J5IGtleSkgPGJh +emVsLWRldkBnb29nbGVncm91cHMuY29tPokCPgQTAQIAKAIbAwYLCQgHAwIGFQgC +CQoLBBYCAwECHgECF4AFAlsGueoFCQeEhaQACgkQPVkZtEhFfuCojRAAqtUaEbK8 +zVAPssZDRPun0k1XB3hXxEoe5kt00cl51F+KLXN2OM5gOn2PcUw4A+Ci+48cgt9b +hTWwWuC9OPn9OCvYVyuTJXT189Pmg+F9l3zD/vrD5gdFKDLJCUPo/tRBTDQqrRGA +JssWIzvGR65O2AosoIcj7VAfNj34CBHm25abNpGnWmkiREZzElLFqjTR+FwAMxyA +VJnPbn+K1zyi9xUZKcL1QzKcHBTPFAdZR6zTII/+03n4wAL/w8+x/A1ocmE7jxCI +cgq7vaHSpGmigU2+TXckUslIgIC64iqYBpPvFAPNlqXmo9rDfL2Imyyuz1ep7j/b +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 +MZaF+SBEZXd1ZSqB5Q== +=laPs +-----END PGP PUBLIC KEY BLOCK----- diff --git a/httputil/httputil.go b/httputil/httputil.go index 26666466..a3e9bf6d 100644 --- a/httputil/httputil.go +++ b/httputil/httputil.go @@ -2,6 +2,7 @@ package httputil import ( + _ "embed" b64 "encoding/base64" "errors" "fmt" @@ -25,6 +26,9 @@ import ( "github.com/bazelbuild/bazelisk/httputil/progress" ) +//go:embed bazel_key.pub.gpg +var VerificationKey string + var ( // DefaultTransport specifies the http.RoundTripper that is used for any network traffic, and may be replaced with a dummy implementation for unit testing. DefaultTransport = http.DefaultTransport @@ -35,86 +39,6 @@ var ( // RetryClock is used for waiting between HTTP request retries. RetryClock = Clock(&realClock{}) - // VerificationKey is the public PGP key used to verify Bazel binary signatures. - VerificationKey = ` ------BEGIN PGP PUBLIC KEY BLOCK----- - -mQINBFdEmzkBEACzj8tMYUau9oFZWNDytcQWazEO6LrTTtdQ98d3JcnVyrpT16yg -I/QfGXA8LuDdKYpUDNjehLtBL3IZp4xe375Jh8v2IA2iQ5RXGN+lgKJ6rNwm15Kr -qYeCZlU9uQVpZuhKLXsWK6PleyQHjslNUN/HtykIlmMz4Nnl3orT7lMI5rsGCmk0 -1Kth0DFh8SD9Vn2G4huddwxM8/tYj1QmWPCTgybATNuZ0L60INH8v6+J2jJzViVc -NRnR7mpouGmRy/rcr6eY9QieOwDou116TrVRFfcBRhocCI5b6uCRuhaqZ6Qs28Bx -4t5JVksXJ7fJoTy2B2s/rPx/8j4MDVEdU8b686ZDHbKYjaYBYEfBqePXScp8ndul -XWwS2lcedPihOUl6oQQYy59inWIpxi0agm0MXJAF1Bc3ToSQdHw/p0Y21kYxE2pg -EaUeElVccec5poAaHSPprUeej9bD9oIC4sMCsLs7eCQx2iP+cR7CItz6GQtuZrvS -PnKju1SKl5iwzfDQGpi6u6UAMFmc53EaH05naYDAigCueZ+/2rIaY358bECK6/VR -kyrBqpeq6VkWUeOkt03VqoPzrw4gEzRvfRtLj+D2j/pZCH3vyMYHzbaaXBv6AT0e -RmgtGo9I9BYqKSWlGEF0D+CQ3uZfOyovvrbYqNaHynFBtrx/ZkM82gMA5QARAQAB -tEdCYXplbCBEZXZlbG9wZXIgKEJhemVsIEFQVCByZXBvc2l0b3J5IGtleSkgPGJh -emVsLWRldkBnb29nbGVncm91cHMuY29tPokCPgQTAQIAKAIbAwYLCQgHAwIGFQgC -CQoLBBYCAwECHgECF4AFAlsGueoFCQeEhaQACgkQPVkZtEhFfuCojRAAqtUaEbK8 -zVAPssZDRPun0k1XB3hXxEoe5kt00cl51F+KLXN2OM5gOn2PcUw4A+Ci+48cgt9b -hTWwWuC9OPn9OCvYVyuTJXT189Pmg+F9l3zD/vrD5gdFKDLJCUPo/tRBTDQqrRGA -JssWIzvGR65O2AosoIcj7VAfNj34CBHm25abNpGnWmkiREZzElLFqjTR+FwAMxyA -VJnPbn+K1zyi9xUZKcL1QzKcHBTPFAdZR6zTII/+03n4wAL/w8+x/A1ocmE7jxCI -cgq7vaHSpGmigU2+TXckUslIgIC64iqYBpPvFAPNlqXmo9rDfL2Imyyuz1ep7j/b -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 -MZaF+SBEZXd1ZSqB5Q== -=laPs ------END PGP PUBLIC KEY BLOCK----- -` - // MaxRetries specifies how often non-fatally failing HTTP requests should be retried. MaxRetries = 4 // MaxRequestDuration defines the maximum amount of time that a request and its retries may take in total