Skip to content

Commit

Permalink
Implement support for BAZELISK_VERIFY_SHA256 (#441)
Browse files Browse the repository at this point in the history
The new BAZELISK_VERIFY_SHA256 variable can be set to the expected SHA256
hash of the downloaded Bazel binary.  If set, then the binary is required
to match the hash before it is used.

This is important for cases where provenance of the artifact cannot be
asserted purely via the HTTPS trust chain (such as what happens in a
mutable artifact repository with lax access controls).
  • Loading branch information
jmmv authored Apr 5, 2023
1 parent 204a69f commit b76d71d
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 3 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ Additionally, a few special version names are supported for our official release

## Where does Bazelisk get Bazel from?

By default Bazelisk retrieves Bazel releases, release candidates and binaries built at green commits from Google Cloud Storage.
By default Bazelisk retrieves Bazel releases, release candidates and binaries built at green commits from Google Cloud Storage. The downloaded artifacts are validated against the SHA256 value recorded in `BAZELISK_VERIFY_SHA256` if this variable is set in the configuration file.

As mentioned in the previous section, the `<FORK>/<VERSION>` version format allows you to use your own Bazel fork hosted on GitHub:

Expand Down Expand Up @@ -149,6 +149,7 @@ The following variables can be set:
- `BAZELISK_SHUTDOWN`
- `BAZELISK_SKIP_WRAPPER`
- `BAZELISK_USER_AGENT`
- `BAZELISK_VERIFY_SHA256`
- `USE_BAZEL_VERSION`

Configuration variables are evaluated with precedence order. The preferred values are derived in order from highest to lowest precedence as follows:
Expand Down
48 changes: 48 additions & 0 deletions bazelisk_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,50 @@ function test_bazel_download_path_go() {
(echo "FAIL: Expected to download bazel binary into specific path."; exit 1)
}

function test_bazel_verify_sha256() {
setup

echo "6.1.1" > .bazelversion

# First try to download and expect an invalid hash (it doesn't matter what it is).
if BAZELISK_HOME="$BAZELISK_HOME" BAZELISK_VERIFY_SHA256="invalid-hash" \
bazelisk version 2>&1 | tee log; then
echo "FAIL: Command should have errored out"; exit 1
fi

grep "need sha256=invalid-hash" log || \
(echo "FAIL: Expected to find hash mismatch"; exit 1)

# IMPORTANT: The mixture of lowercase and uppercase letters in the hashes below is
# intentional to ensure the variable contents are normalized before comparison.
# If updating these values, re-introduce randomness.
local os="$(uname -s | tr A-Z a-z)"
case "${os}" in
darwin)
expected_sha256="038e95BAE998340812562ab8d6ada1a187729630bc4940a4cd7920cc78acf156"
;;
linux)
expected_sha256="651a20d85531325df406b38f38A1c2578c49D5e61128fba034f5b6abdb3d303f"
;;
msys*|mingw*|cygwin*)
expected_sha256="1d997D344936a1d98784ae58db1152d083569556f85cd845e6e340EE855357f9"
;;
*)
echo "FAIL: Unknown OS ${os} in test"
exit 1
;;
esac

# Now try the same download as before but with the correct hash expectation. Note that the
# hash has a random uppercase / lowercase mixture to ensure this does not impact equality
# checks.
BAZELISK_HOME="$BAZELISK_HOME" BAZELISK_VERIFY_SHA256="${expected_sha256}" \
bazelisk version 2>&1 | tee log

grep "Build label:" log || \
(echo "FAIL: Expected to find 'Build label' in the output of 'bazelisk version'"; exit 1)
}

function test_bazel_download_path_py() {
setup

Expand Down Expand Up @@ -411,6 +455,10 @@ if [[ $BAZELISK_VERSION == "GO" ]]; then
test_bazel_download_path_go
echo

echo '# test_bazel_verify_sha256'
test_bazel_verify_sha256
echo

echo "# test_bazel_prepend_binary_directory_to_path_go"
test_bazel_prepend_binary_directory_to_path_go
echo
Expand Down
41 changes: 39 additions & 2 deletions core/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -411,18 +411,55 @@ func downloadBazelIfNecessary(version string, baseDirectory string, repos *Repos
}

destDir := filepath.Join(baseDirectory, pathSegment, "bin")
expectedSha256 := strings.ToLower(GetEnvOrConfig("BAZELISK_VERIFY_SHA256"))

tmpDestFile := "bazel-tmp" + platforms.DetermineExecutableFilenameSuffix()
destFile := "bazel" + platforms.DetermineExecutableFilenameSuffix()

destPath := filepath.Join(destDir, destFile)
if _, err := os.Stat(destPath); err == nil {
return destPath, nil
}

var tmpDestPath string
if url := GetEnvOrConfig(BaseURLEnv); url != "" {
return repos.DownloadFromBaseURL(url, version, destDir, destFile)
tmpDestPath, err = repos.DownloadFromBaseURL(url, version, destDir, tmpDestFile)
} else {
tmpDestPath, err = downloader(destDir, tmpDestFile)
}
if err != nil {
return "", err
}

return downloader(destDir, destFile)
if len(expectedSha256) > 0 {
f, err := os.Open(tmpDestPath)
if err != nil {
os.Remove(tmpDestPath)
return "", fmt.Errorf("cannot open %s after download: %v", tmpDestPath, err)
}
defer os.Remove(tmpDestPath)
// We cannot defer f.Close() because keeping the handle open when we try to do the
// rename later on fails on Windows.

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)
}
f.Close()

actualSha256 := strings.ToLower(fmt.Sprintf("%x", h.Sum(nil)))
if expectedSha256 != actualSha256 {
return "", fmt.Errorf("%s has sha256=%s but need sha256=%s", tmpDestPath, actualSha256, expectedSha256)
}
}

// Only place the downloaded binary in its final location once we know it is fully downloaded
// and valid, to prevent invalid files from ever being executed.
if err = os.Rename(tmpDestPath, destPath); err != nil {
return "", fmt.Errorf("cannot rename %s to %s: %v", tmpDestPath, destPath, err)
}
return destPath, nil
}

func copyFile(src, dst string, perm os.FileMode) error {
Expand Down

0 comments on commit b76d71d

Please sign in to comment.