Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Authenticate downloaded binaries #17

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
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
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

**A user-friendly launcher for Bazel.**

Bazelisk is a wrapper for Bazel. It automatically picks a good version of Bazel given your current working directory, downloads it from the official server (if required) and then transparently passes through all command-line arguments to the real Bazel binary. You can call it just like you would call Bazel.
Bazelisk is a wrapper for Bazel. It automatically picks a good version of Bazel given your current working directory, downloads it from the official server (if required) and then transparently passes through all command-line arguments to the real Bazel binary. You can call it just like you would call Bazel. If you have [`gpg`][GnuPG] installed, Bazelisk will authenticate all Bazel downloads.

Bazelisk is currently not an official part of Bazel and is not tested or code reviewed as thoroughly as Bazel itself. It's a personal project that @philwo (a core contributor to Bazel) wrote in his free time. If users like it, we might merge it into the bazelbuild organization and make it an official tool.

Expand All @@ -29,6 +29,10 @@ In the future I will add support for release candidates and for building Bazel f

For ease of use, Bazelisk is written to work with Python 2.7 and 3.x and only uses modules provided by the standard library.

If [GnuPG] is installed and `gpg` is available on the system path, Bazelisk will verify the integrity of the binaries that it downloads.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't true for the Go version of bazelisk.


[GnuPG]: https://www.gnupg.org/

## Ideas for the future

- Add a Homebrew recipe for Bazelisk to make it easy to install on macOS.
Expand Down
243 changes: 232 additions & 11 deletions bazelisk.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
limitations under the License.
"""

import collections
from contextlib import closing
from distutils.version import LooseVersion
import json
Expand All @@ -24,6 +25,7 @@
import shutil
import subprocess
import sys
import tempfile
import time

try:
Expand All @@ -34,6 +36,10 @@

ONE_HOUR = 1 * 60 * 60

# Bazelisk exits with this code when GPG is installed but the binary
# cannot be authenticated.
AUTHENTICATION_FAILURE_EXIT_CODE = 2


def decide_which_bazel_version_to_use():
# Check in this order:
Expand Down Expand Up @@ -116,25 +122,160 @@ def normalized_machine_arch_name():
return machine


def determine_url(version, bazel_filename):
SubprocessResult = collections.namedtuple("SubprocessResult", ("exit_code",))


def subprocess_run(command, input=None, error_message=None):
"""Kind of like Python 3's subprocess.run, but works in Python 2.

The contents of stdout and stderr are captured. If the command
succeeds (exit code 0), they are not printed. If the command fails,
stderr is printed along with the provided error message (if any).

Args:
command: The command to be executed, as a list of strings
input: A bytestring to use as stdin, or None.
error_message: If not None, will be logged on failure.

Returns:
A `SubprocessResult` including the process's exit code.
"""
process = subprocess.Popen(
command,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
(stdout, stderr) = process.communicate(input=input)
exit_code = process.wait()
if exit_code != 0 and error_message is not None:
if error_message is not None:
sys.stderr.write("bazelisk: {}\n".format(error_message))
write_binary_to_stderr(stderr)
return SubprocessResult(exit_code=exit_code)


def write_binary_to_stderr(bytestring):
# Python 2 compatibility hack. In Python 3, you can't write byte
# strings to stdio; instead, you have to use the `sys.stderr.buffer`
# attribute, which is not available in Python 2.
buffer = getattr(sys.stderr, "buffer", sys.stderr)
buffer.write(bytestring)


def verify_authenticity(binary_path, signature_path):
"""Authenticate a binary and signature against the Bazel public key.

This will use a fresh temporary keyring populated only with the
Bazel team's signing key; it is independent of any existing PGP data
or settings that the user may have.

Args:
binary_path: File path to the Bazel binary to be executed.
signature_path: File path to the detached signature made by the
Bazel release PGP key to sign the provided binary.

Returns:
True if the binary is valid or gpg is not installed; False if gpg is
installed but we cannot determine that the binary is valid.
"""
if subprocess_run(
["gpg", "--batch", "--version"],
error_message=
"Warning: skipping authenticity check because GPG is not installed.",
).exit_code != 0:
return True
Comment on lines +181 to +186

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there should be a "project-owner" way of turning this into an error by default in order to use this on CI systems, for example, with confidence that the build is always built with an untainted bazel. I think using the .bazelversion file to specify a flag, something like --require-gpg-verification, could do the trick nicely.


tempdir = tempfile.mkdtemp(prefix="tmp_bazelisk_gpg_")
try:
gpg_invocation = [
"gpg",
"--batch",
"--no-default-keyring",
"--homedir",
tempdir,
]

# DO NOT SUBMIT: Debugging on Windows and macOS...
print("tempdir: {}\n".format(tempdir))
print("gpg version:\n")
subprocess.call(gpg_invocation + ["--version"])
print("gpg location:\n")
subprocess.call(["which", "gpg"])

if subprocess_run(
gpg_invocation + ["--import-ownertrust"],
input=BAZEL_ULTIMATE_OWNERTRUST,
error_message="Failed to initialize GPG keyring").exit_code != 0:
return False
if subprocess_run(
gpg_invocation + ["--import"],
input=BAZEL_PUBLIC_KEY,
error_message="Failed to import Bazel public key").exit_code != 0:
return False
if subprocess_run(
gpg_invocation + ["--verify", signature_path, binary_path],
error_message="Failed to authenticate binary!").exit_code != 0:
return False
sys.stderr.write("Verified authenticity.\n")
return True

finally:
shutil.rmtree(tempdir)


DownloadUrls = collections.namedtuple("DownloadUrls",
("binary_url", "signature_url"))


def determine_urls(version, bazel_filename):
# Split version into base version and optional additional identifier.
# Example: '0.19.1' -> ('0.19.1', None), '0.20.0rc1' -> ('0.20.0', 'rc1')
(version, rc) = re.match(r'(\d*\.\d*(?:\.\d*)?)(rc\d)?', version).groups()
return "https://releases.bazel.build/{}/{}/{}".format(
binary_url = "https://releases.bazel.build/{}/{}/{}".format(
version, rc if rc else "release", bazel_filename)
signature_url = "{}.sig".format(binary_url)
return DownloadUrls(binary_url=binary_url, signature_url=signature_url)


def download_file(url, destination_path):
"""Download a file from the given URL, saving it to the given path."""
sys.stderr.write("Downloading {}...\n".format(url))
with closing(urlopen(url)) as response:
with open(destination_path, 'wb') as out_file:
shutil.copyfileobj(response, out_file)


def download_bazel_into_directory(version, directory):
"""Download and authenticate the specified version of Bazel.

If the binary already exists, it will not be re-downloaded.

If the binary does not exist, it and its signature will be downloaded.
The binary will only be saved and made executable if the signature is
valid (or if we are unable to validate the signature because GPG is
not installed).

If the signature is invalid, a `SystemExit` exception will be raised.

Returns:
The path to the valid, executable Bazel binary within the provided
directory.
"""
bazel_filename = determine_bazel_filename(version)
url = determine_url(version, bazel_filename)
destination_path = os.path.join(directory, bazel_filename)
if not os.path.exists(destination_path):
sys.stderr.write("Downloading {}...\n".format(url))
with closing(urlopen(url)) as response:
with open(destination_path, 'wb') as out_file:
shutil.copyfileobj(response, out_file)
os.chmod(destination_path, 0o755)
return destination_path
urls = determine_urls(version, bazel_filename)
binary_path = os.path.join(directory, bazel_filename)
if not os.path.exists(binary_path):
untrusted_binary_path = "{}.untrusted".format(binary_path)
signature_path = "{}.sig".format(binary_path)
download_file(urls.binary_url, untrusted_binary_path)
download_file(urls.signature_url, signature_path)
if verify_authenticity(untrusted_binary_path, signature_path):
os.rename(untrusted_binary_path, binary_path)
else:
os.unlink(untrusted_binary_path)
raise SystemExit(AUTHENTICATION_FAILURE_EXIT_CODE)
os.chmod(binary_path, 0o755)
return binary_path


def maybe_makedirs(path):
Expand Down Expand Up @@ -179,5 +320,85 @@ def main(argv=None):
return execute_bazel(bazel_path, argv[1:])


BAZEL_ULTIMATE_OWNERTRUST = b"71A1D0EFCFEB6281FD0437C93D5919B448457EE0:6:\n"

BAZEL_PUBLIC_KEY = b"""\
-----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
emVsLWRldkBnb29nbGVncm91cHMuY29tPokCHAQQAQgABgUCWBNy9QAKCRDdPvlj
mR8ewjP7D/9B9pm7jjwxVfvc7Rw1w9wu+3R94X9pmZAt6Jl5BvhOkHNM/oKM2Q4P
6oRyzJDAHUAirFIkUeW9kxbsB01O+ryS6BUR6pKFK2vxliqiOGuZ1Ha65nl6JsL5
UXQGrE7fZ3/I6QuNv6IodmBQypoQB/RZ4AORZGhuAE9Acuxw4oZLAB95vcFf8hMS
BCLDmYZknINjeh3wz+IjqR8hhJ4IgSWXpy/Ju7LHlSOK7G2ipXCeOdBVb0b+oHYR
V2vuwwxioH0bneIsoxKKZ7KrcVT1aRM0CK+uiDLMJyOTCSXhg5z19UGmbEIP3xhU
BeiGpKbfHsv6DB97hGQDxGlWRjVSY4bx7SNXkAsd4XPStkjwwgqMqWLEAaUltDQ/
Ur0Ye2hQjnkZcV5ivnrtki8Rj7MhaaJZDaNRqjxtc263uMn5Tyq2eY4HddjY/KXL
kReaPBkiU+Q9kVyWlcp0LnNVGcpkwNGOrk+fSdlDmzXEYenermqbEj/I+ENaF2aC
aSuI4KquRGj3pPPYD3Yl4CAH1+igKmKq0QeThAtXLaBKl8ZO+ZJpGQ7muDhpJH9m
xNTDEkPSutattuaOnMrM9uF5S5oKK5OX9S8aADmbb0qNEm4KOKv84zKf0zCkprjP
u8nHLxp3GJ+G2VVGdzv3tWVoz2iIUVEn0FM+Aaj9tVqvHerHpTErAYkCPgQTAQIA
KAIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AFAlsGueoFCQeEhaQACgkQPVkZ
tEhFfuCojRAAqtUaEbK8zVAPssZDRPun0k1XB3hXxEoe5kt00cl51F+KLXN2OM5g
On2PcUw4A+Ci+48cgt9bhTWwWuC9OPn9OCvYVyuTJXT189Pmg+F9l3zD/vrD5gdF
KDLJCUPo/tRBTDQqrRGAJssWIzvGR65O2AosoIcj7VAfNj34CBHm25abNpGnWmki
REZzElLFqjTR+FwAMxyAVJnPbn+K1zyi9xUZKcL1QzKcHBTPFAdZR6zTII/+03n4
wAL/w8+x/A1ocmE7jxCIcgq7vaHSpGmigU2+TXckUslIgIC64iqYBpPvFAPNlqXm
o9rDfL2Imyyuz1ep7j/bJrsOxVKwHO8HfgE2WcvcEmkjQ3kpW+qVflwPKsfKRN6o
e1rX5l9MxS/nGPok4BIIV9Y82K3o8Yu0KUgbHhEsITNizBgeJSIEhbF9YAmMeBie
6zRnsOKmOqnx2Y9OAfU7QhpUoO9DBVk/c3KkiOSf6RYxjrLmou/tLKdsQaenKTDO
H8fQTexnMYxRlp5yU1+9eZOdJeRDm078tGB+IRWB3QElIgYiRbCd8VzgDsMJJQbQ
2VdQlVaZL84d6Zntk2pLa4HDB4nE+UpfoLcT7iM9hqn9b7NHzmHiPVJecNNGjLTv
xZ1sW7+0S7oo7lOMrEPpk84DXEqg20Cb3D7YKirwR7qi/StTdil3bYKJAj4EEwEC
ACgFAldEmzkCGwMFCQPCZwAGCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJED1Z
GbRIRX7gPgQP/RK1T5Am628Bl+hx2NofUVC5zrgTiSoag3ZQJifQYtU8JYhu9q1z
udpju1m0ieFMfuW2zt5Is1CHesa+hWZkyYhwmONoMICzhyyMHemO5ftj08kNK8i9
+YYj75cIXCeEM3xdP/DEw78kGongSkEJGQ/kZlyS5gxps7S4WlMNAU5DjX2zdI03
SLe5QJpFWWKPCQqDGwl5ZPZJIepcfb12dCUJH5tYEUVgAEobDhzGGYF7I9dWNwu+
s8b/IzaE/N9eUOG1TCpo6/mzmke4nYk5cUSpde3ka/KmdQKia8MsMoxU1kKcB8N7
keIjLfkEoHHiorooaWQab5lbTVWjIiU5Eet6UZsGhqBqL+Lt1TAUWumDEGl0NVBM
K6hB/nMjWYFonZSMsKYMw+IYy2LhP3QrwlU/jN9r00FYTQwsGZJojXlUBNUf+QHY
UC0rwZyxlyH5F57ApdLxZaBtm64MSLN0zfKBIrSHlVifqI+QKk0QXhyGeTB5LRf9
fAHzzREeFLbnxsVFmwLcn9x7ZmWdN1zHLedqkmimW02NWzprIMum2typHPYn42Gv
cPRRFcrFD5i88uPpdyuV8PdolWCw7Qk04YWH20yfCFryRhPYZMmJjxeENDt5BUKk
JqxVQAzMsUdAzCFC7PFN5GymuSt/d/WkmF0AHaaunek9Mtvl3b0h65lbuQINBFdE
mzkBEADnn/VUGUOlX8SVTIuZHI8LP9X6awd2KfLDgy/kMlC6m5nCUzK/E8/Nzsaa
wh+TXO47MNaKs2zbavjdqTp2wC+lxT6JUGLjoypRxs20L6R/GUqJOgM8Kzzat18K
AdPtdgPOsJaWo1D374GozY5hEjHIS5yLN6h9Y+WslSAq+7x9YtVnptifgv8+oCGh
uG5KNFygHlOnzWEZrhyxwogYiqHKZ+eC5pjy+Inze0c9SpAmgCk0/LyrWlYdINXr
MG/vVt6pXZvpwHOntWo1g4i6oTpk0EVa4IbfNFz2Jrb9sfHHZMYBAm0+k/OK2bTG
QHcYY3TpiedIIT5/aP7sXQg7q4WVLLuGjQ+hIVsOBH3WQkrdLRkFnHgfbVwilZYH
N/013Uzgfc7sqGcZJkOrr61dn38X9lS8JkelCUl7AM9j3fXliZpx/kJmTzF4TlRQ
jEUx07EXwHsi1vqtsVa/63NZ1f/T8zz9vRkmFW+eBbO6H9qB1LgTlqd/tqEZYz3q
M9EhARv0NE3Zgan2E6JfaxmqSHETnNaoPaB1enZkDEwJMd/iKPM5Ww6d17tvkGoL
QkvveA3B/WI4fIIDOoaIV9qHz+h0FMOEyx1UyyNIjHNzCXBGfPL6EGx1ik/X2J4k
IygRElNtSBKyk92Fj9jgKHOUUeOIAphPq9eJhwpLTiy0K0erAQARAQABiQIlBBgB
AgAPAhsMBQJbBrn9BQkHhIXBAAoJED1ZGbRIRX7gjHAP/RkbT0nWtn1vOGV6HPUK
10GJeiama/usApktNvRdzw+zhxNxdmnhXvnmSFjhaUBuiChy22dl22J8wH0gQE6Z
041C2w5QJO3RQSFhGTLuWj/Axr3bbBixPg2Y4i6MmgrEIrFqeLyDsYlwZ8pgMohX
GOe9AiT5u/1qKOQAnTWB1uuyXauykyKTMvZq075CXHHS64n5LHXZSg5K3FEskOoR
xw5rQHTRsg+lp5v+mMe5UTNbIUMisWDtUcBZmgdZbBuufuYCnO8F4MjrccgG1ihc
bG22gUrbz6NGpgbiMZ6a0HuwhCnHPdEiBuSYuL+shMnwbhuW0fdlA8PKyIS6/Zwa
a7VK/O57AFNZsRSaBhBZl3pCGaecdwL2cPfTrTcfBxn9NotAygDBNHPzwlCHJLdn
qZbmbNgww7iBtHhthV/jCQxhK7ek5LcHKM9nekMYdEwGfQ4fXIu/9BRXMmAshook
N/TK/MTNVPdXX/b8I6uv53orE7EzIZKsM5Ew9ujc6Cc/fKGrg5wfYTXgSgl+2wPd
vyAGebWM3kgbLW9dnfi3xqU6Ol5evz49MRqjGxPADXzosed1ILZuGTg8sp0u6oHm
QUgn3aEE61DcXTtsSbieQUFZwTHG2F8VWLmmW/lSoqFSjrGneyjAk8eVLHgPwDxL
n5VZt+ds9MenAEZScDuR4Usd
=j+Xa
-----END PGP PUBLIC KEY BLOCK-----
"""

if __name__ == '__main__':
sys.exit(main())