-
Notifications
You must be signed in to change notification settings - Fork 320
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
base: master
Are you sure you want to change the base?
Authenticate downloaded binaries #17
Conversation
There was a problem hiding this 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. :)
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? :) |
4e94b1d
to
1d63385
Compare
No worries, and glad to hear it! I’ve rebased and pushed, and the tests pass on Linux but fail on macOS
even when I add a log statement to the very top of The Windows failure looks like it might be a bug in GPG: when invoked
and then is confused when that directory does not exist. That’s somewhat I’ll look into the Windows failure when I get a chance. Do you know what |
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 |
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
95c7248
to
e67624f
Compare
this looks like it needs to be rebased |
There was a problem hiding this 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)
if subprocess_run( | ||
["gpg", "--batch", "--version"], | ||
error_message= | ||
"Warning: skipping authenticity check because GPG is not installed.", | ||
).exit_code != 0: | ||
return True |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
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; wedon’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:
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 thesignature.
Remove the
~/.bazelisk
directory. Symlink/bin/false
to~/bin/gpg
, and ensure that the symlink precedes the realgpg
onyour 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. Editbazelisk.py
, changing thedetermine_urls
function so that the returnedbinary_url
is anarbitrary web page (like
http://example.com/
) but the signatureURL 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 theinvalid binary (though the signature is still there). Revert the
changes to
bazelisk.py
.Remove the
~/.bazelisk
directory. Create an arbitrary document anduse
gpg --detach-sign
to sign it with a key that is not the Bazelsigning key. Spawn a web server (
python -m SimpleHTTPServer
) toserve the “malicious executable” and its signature. Edit
bazelisk.py
, changing thedetermine_urls
function to point boththe 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.