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

Conversation

wchargin
Copy link

Summary:
This pull request adds logic to authenticate all Bazel binaries that are
downloaded, as long as the user has GPG installed. If a user does not
have GPG installed, a new warning will be printed when a binary is
downloaded, but Bazelisk will function the same way as before. (GPG is
installed by default on Debian and Ubuntu.)

No new subprocesses are spawned when an already-downloaded version of
Bazel is run. The only appreciable overhead is incurred at download
time.

The Bazel public key and ownertrust information are included directly
into the bazelisk.py file so that the script can be self-contained; we
don’t have to worry about bundling it with any external assets.

Resolves #15.

Test Plan:
Download https://bazel.build/bazel-release.pub.gpg and verify that it
exactly matches the blob checked into this pull request:

$ curl --silent 'https://bazel.build/bazel-release.pub.gpg' | shasum -a 256
30af2ca7abfb65987cd61802ca6e352aadc6129dfb5bfc9c81f16617bc3a4416  -
$ sed -n '/BEGIN PGP/,/END PGP/p' bazelisk.py | shasum -a 256
30af2ca7abfb65987cd61802ca6e352aadc6129dfb5bfc9c81f16617bc3a4416  -

Then, follow these steps to check the behavior of the authentication:

  • Remove the ~/.bazelisk directory. Run ./bazelisk.py version.
    Note that it downloads the latest binary and the latest signature,
    then prints “Authenticity verified” before invoking Bazel.

  • Run ./bazelisk.py version again. Note that it does not verify the
    signature.

  • Remove the ~/.bazelisk directory. Symlink /bin/false to
    ~/bin/gpg, and ensure that the symlink precedes the real gpg on
    your path. Run Bazelisk, and note that it prints a warning that GPG
    is not available but executes Bazel anyway. Run Bazelisk again, and
    note that it does not print the warning (because it reuses the
    existing executable without reauthenticating). Remove the symlink.

  • Remove the ~/.bazelisk directory. Edit bazelisk.py, changing the
    determine_urls function so that the returned binary_url is an
    arbitrary web page (like http://example.com/) but the signature
    URL is unchanged. Run Bazelisk, and note that Bazelisk reports,
    “Failed to authenticate binary!”, includes the GPG output (“BAD
    signature”), and aborts with exit code 2 without invoking Bazel.
    Run ls ~/.bazelisk/bin and note that it does not include the
    invalid binary (though the signature is still there). Revert the
    changes to bazelisk.py.

  • Remove the ~/.bazelisk directory. Create an arbitrary document and
    use gpg --detach-sign to sign it with a key that is not the Bazel
    signing key. Spawn a web server (python -m SimpleHTTPServer) to
    serve the “malicious executable” and its signature. Edit
    bazelisk.py, changing the determine_urls function to point both
    the binary and the signature to this local web server. Run Bazelisk,
    and note that it fails to authenticate the binary, with the message
    “public key not found”.

Repeat the above steps in Python 2 and Python 3.

Verify that your personal GnuPG database has not been modified (in
particular, the Bazel key should not have been installed, and the trust
settings should not have been modified).

I have tested this on Linux with gpg (GnuPG) 1.4.20. I don’t see any
reason that it shouldn’t work on macOS or Windows as long as the gpg(1)
interfaces are the same.

Copy link
Member

@philwo philwo left a comment

Choose a reason for hiding this comment

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

Code looks great, thanks a lot! I'll test it on my machines and will merge it then. :)

@philwo
Copy link
Member

philwo commented Jan 18, 2019

Hey! Sorry that it took me so long, but I finally added tests, a Buildkite CI config and they even pass on all three platforms. Would you mind rebasing and pushing, so that they run for your PR as well? :)

@wchargin wchargin force-pushed the wchargin-authenticate-binaries branch from 4e94b1d to 1d63385 Compare January 18, 2019 19:27
@wchargin
Copy link
Author

No worries, and glad to hear it!

I’ve rebased and pushed, and the tests pass on Linux but fail on macOS
and Windows. I can’t find a way to view the output of the macOS
failures; I’m not even sure that the test target is being run. The only
error that I see is

[Errno 13] Permission denied: '/bazelisk_test'

even when I add a log statement to the very top of bazelisk_test.py.
But I see that the Buildkite tests pass on master, so it’s not clear to
me why we’d have trouble invoking the test target.

The Windows failure looks like it might be a bug in GPG: when invoked
with gpg --homedir D:\temp\foo, GPG is resolving its homedir to

Home: /d/temp/Bazel.runfiles_yq104_1d/runfiles/__main__/D:\temp\tmp_bazelisk_gpg_vmqe5mra

and then is confused when that directory does not exist. That’s somewhat
unfortunate, but I can probably work around it.

I’ll look into the Windows failure when I get a chance. Do you know what
might be going on with the macOS failure, or at least how I can see any
useful test output?

@philwo
Copy link
Member

philwo commented Jan 19, 2019

Thanks for the update!

I have no idea what that weird error on macOS is - it seems to come from Bazel after the test ran (or maybe from our CI script?). I'll look into it when I have some time.

I've modified the test and also added the flag --test_output=streamed on macOS, so you should see the output in the Buildkite log now, even if Bazel crashes afterwards. :)

Summary:
This includes the full public key for the Bazel team (48457EE0) as well
as the ownertrust entry to mark the key as ultimately trusted within a
keyring.

The contents are included directly into the `bazelisk.py` file so that
the script can be self-contained; we don’t have to worry about bundling
it with any external assets.

The ownertrust format was created by running importing the Bazel key
into a clean keyring, running `gpg --edit-key 48457EE0` and setting the
trust level to “ultimate”, then invoking `gpg --export-ownertrust`.

Test Plan:
Download <https://bazel.build/bazel-release.pub.gpg> and verify that it
exactly matches the blob checked into this commit:

    $ curl --silent 'https://bazel.build/bazel-release.pub.gpg' | shasum -a 256
    30af2ca7abfb65987cd61802ca6e352aadc6129dfb5bfc9c81f16617bc3a4416  -
    $ sed -n '/BEGIN PGP/,/END PGP/p' bazelisk.py | shasum -a 256
    30af2ca7abfb65987cd61802ca6e352aadc6129dfb5bfc9c81f16617bc3a4416  -

wchargin-branch: bazel-pgp-info
Summary:
This commit adds logic to authenticate all Bazel binaries that are
downloaded, as long as the user has GPG installed. If a user does not
have GPG installed, a new warning will be printed when a binary is
downloaded, but Bazelisk will function the same way as before. (GPG is
installed by default on Debian and Ubuntu.)

No new subprocesses are spawned when an already-downloaded version of
Bazel is run. The only appreciable overhead is incurred at download
time.

Resolves bazelbuild#15.

Test Plan:

  - Remove the `~/.bazelisk` directory. Run `./bazelisk.py version`.
    Note that it downloads the latest binary and the latest signature,
    then prints “Authenticity verified” before invoking Bazel.

  - Run `./bazelisk.py version` again. Note that it does not verify the
    signature.

  - Remove the `~/.bazelisk` directory. Symlink `/bin/false` to
    `~/bin/gpg`, and ensure that the symlink precedes the real `gpg` on
    your path. Run Bazelisk, and note that it prints a warning that GPG
    is not available but executes Bazel anyway. Run Bazelisk again, and
    note that it does not print the warning (because it reuses the
    existing executable without reauthenticating). Remove the symlink.

  - Remove the `~/.bazelisk` directory. Edit `bazelisk.py`, changing the
    `determine_urls` function so that the returned `binary_url` is an
    arbitrary web page (like `http://example.com/`) but the signature
    URL is unchanged. Run Bazelisk, and note that Bazelisk reports,
    “Failed to authenticate binary!”, includes the GPG output (“BAD
    signature”), and aborts with exit code 2 _without_ invoking Bazel.
    Run `ls ~/.bazelisk/bin` and note that it does not include the
    invalid binary (though the signature is still there). Revert the
    changes to `bazelisk.py`.

  - Remove the `~/.bazelisk` directory. Create an arbitrary document and
    use `gpg --detach-sign` to sign it with a key that is not the Bazel
    signing key. Spawn a web server (`python -m SimpleHTTPServer`) to
    serve the “malicious executable” and its signature. Edit
    `bazelisk.py`, changing the `determine_urls` function to point both
    the binary and the signature to this local web server. Run Bazelisk,
    and note that it fails to authenticate the binary, with the message
    “public key not found”.

Repeat the above steps in Python 2 and Python 3.

Verify that your personal GnuPG database has not been modified (in
particular, the Bazel key should not have been installed, and the trust
settings should not have been modified).

I have tested this on Linux with gpg (GnuPG) 1.4.20. I don’t see any
reason that it shouldn’t work on macOS or Windows as long as the gpg(1)
interfaces are the same.

wchargin-branch: authenticate-binaries
Summary:
The Linux tests passed, but the macOS tests failed with no useful
output, and the Windows tests failed with an unexpected GPG invocation
error with a strange path. This commit adds some print-statements in an
attempt to investigate.

Test Plan:
Running `./bazelisk version` and `bazel test :bazelisk_test` both still
work on Python 2 and 3 on Linux.

wchargin-branch: authenticate-binaries
wchargin-branch: authenticate-binaries
wchargin-branch: authenticate-binaries
@wchargin wchargin force-pushed the wchargin-authenticate-binaries branch from 95c7248 to e67624f Compare January 20, 2019 01:16
@tmc
Copy link

tmc commented Feb 5, 2019

this looks like it needs to be rebased

Copy link

@pmuetschard pmuetschard left a comment

Choose a reason for hiding this comment

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

Just a drive by comment. (I filed #97 and was referred to this)

Comment on lines +181 to +186
if subprocess_run(
["gpg", "--batch", "--version"],
error_message=
"Warning: skipping authenticity check because GPG is not installed.",
).exit_code != 0:
return True

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.

@@ -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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Signature verification support
5 participants