Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +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",
)
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,10 @@ 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.

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.)
Expand All @@ -265,10 +269,12 @@ 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`
- `BAZELISK_USER_AGENT`
- `BAZELISK_VERIFICATION_KEY_FILE`
- `BAZELISK_VERIFY_SHA256`
- `USE_BAZEL_VERSION`

Expand Down
2 changes: 2 additions & 0 deletions core/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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",
],
)

Expand All @@ -31,6 +32,7 @@ go_test(
embed = [":core"],
deps = [
"//config",
"//httputil/httputil_test_helper",
"//platforms",
],
)
125 changes: 108 additions & 17 deletions core/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -457,25 +459,95 @@ 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 {
return "", fmt.Errorf("%s has sha256=%s but need sha256=%s", pathToBazelInCAS, downloadedDigest, expectedSha256)
}
}

// Verifying authenticity of downloaded binary (if it was requested)
if err := verifyBinaryAuthenticity(pathToBazelInCAS, pathToSignatureInCAS, config); 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)
}

return pathToBazelInCAS, nil
}

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)
}
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()

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"
}

verificationResult, err := httputil.VerifyBinary(binary, signature, verificationKey)
if err != nil {
return err
}

if err = verificationResult.SignatureError(); err != nil {
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 {
log.Printf("Signed by \"%s\"", identity)
}

return nil
}

func atomicWriteFile(path string, contents []byte, perm os.FileMode) error {
parent := filepath.Dir(path)
if err := os.MkdirAll(parent, 0755); err != nil {
Expand Down Expand Up @@ -525,43 +597,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)))
Expand All @@ -570,24 +647,37 @@ 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)
}

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 pathToBazelInCAS, actualSha256, nil
return httputil.DownloadArtifact{BinaryPath: pathToBazelInCAS, SignaturePath: pathToSignatureInCAS}, actualSha256, nil
}

func copyFile(src, dst string, perm os.FileMode) error {
Expand Down Expand Up @@ -1395,10 +1485,11 @@ 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)
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
Expand Down
81 changes: 81 additions & 0 deletions core/core_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"testing"

"github.com/bazelbuild/bazelisk/config"
"github.com/bazelbuild/bazelisk/httputil/httputil_test_helper"
"github.com/bazelbuild/bazelisk/platforms"
)

Expand Down Expand Up @@ -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")
}
})
}
Loading