Merge pull request #17220 from woodruffw-forks/ww/refine-gh-auth

attestations: improve authentication techniques
This commit is contained in:
Mike McQuaid 2024-05-06 08:38:07 +01:00 committed by GitHub
commit 3d31594e39
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 78 additions and 11 deletions

View File

@ -33,6 +33,12 @@ module Homebrew
# @api private
class InvalidAttestationError < RuntimeError; end
# Raised if attestation verification cannot continue due to missing
# credentials.
#
# @api private
class GhAuthNeeded < RuntimeError; end
# Returns a path to a suitable `gh` executable for attestation verification.
#
# @api private
@ -56,6 +62,7 @@ module Homebrew
# `https://github/OWNER/REPO/.github/workflows/WORKFLOW.yml@REF` format.
#
# @return [Hash] the JSON-decoded response.
# @raise [GhAuthNeeded] on any authentication failures
# @raise [InvalidAttestationError] on any verification failures
#
# @api private
@ -69,9 +76,18 @@ module Homebrew
cmd += ["--cert-identity", signing_workflow] if signing_workflow.present?
# Fail early if we have no credentials. The command below invariably
# fails without them, so this saves us a network roundtrip before
# presenting the user with the same error.
credentials = GitHub::API.credentials
raise GhAuthNeeded, "missing credentials" if credentials.blank?
begin
output = Utils.safe_popen_read(*cmd)
output = Utils.safe_popen_read({ "GH_TOKEN" => credentials }, *cmd)
rescue ErrorDuringExecution => e
# Even if we have credentials, they may be invalid or malformed.
raise GhAuthNeeded, "invalid credentials" if e.status.exitstatus == 4
raise InvalidAttestationError, "attestation verification failed: #{e}"
end
@ -100,6 +116,7 @@ module Homebrew
# This is a specialization of `check_attestation` for homebrew-core.
#
# @return [Hash] the JSON-decoded response
# @raise [GhAuthNeeded] on any authentication failures
# @raise [InvalidAttestationError] on any verification failures
#
# @api private

View File

@ -1261,6 +1261,16 @@ on_request: installed_on_request?, options:)
ohai "Verifying attestation for #{formula.name}"
begin
Homebrew::Attestation.check_core_attestation formula.bottle
rescue Homebrew::Attestation::GhAuthNeeded
raise CannotInstallFormulaError, <<~EOS
The bottle for #{formula.name} could not be verified.
This typically indicates a missing GitHub API token, which you
can resolve either by setting `HOMEBREW_GITHUB_API_TOKEN` or
by running:
gh auth login
EOS
rescue Homebrew::Attestation::InvalidAttestationError => e
raise CannotInstallFormulaError, <<~EOS
The bottle for #{formula.name} has an invalid build provenance attestation.

View File

@ -4,6 +4,9 @@ require "diagnostic"
RSpec.describe Homebrew::Attestation do
let(:fake_gh) { Pathname.new("/extremely/fake/gh") }
let(:fake_gh_creds) { "fake-gh-api-token" }
let(:fake_error_status) { instance_double(Process::Status, exitstatus: 1, termsig: nil) }
let(:fake_auth_status) { instance_double(Process::Status, exitstatus: 4, termsig: nil) }
let(:cached_download) { "/fake/cached/download" }
let(:fake_bottle_filename) { instance_double(Bottle::Filename, to_s: "fakebottle--1.0.faketag.bottle.tar.gz") }
let(:fake_bottle_url) { "https://example.com/#{fake_bottle_filename}" }
@ -61,11 +64,24 @@ RSpec.describe Homebrew::Attestation do
.and_return(fake_gh)
end
it "raises without any gh credentials" do
expect(GitHub::API).to receive(:credentials)
.and_return(nil)
expect do
described_class.check_attestation fake_bottle,
described_class::HOMEBREW_CORE_REPO
end.to raise_error(described_class::GhAuthNeeded)
end
it "raises when gh subprocess fails" do
expect(GitHub::API).to receive(:credentials)
.and_return(fake_gh_creds)
expect(Utils).to receive(:safe_popen_read)
.with(fake_gh, "attestation", "verify", cached_download, "--repo",
.with({ "GH_TOKEN" => fake_gh_creds }, fake_gh, "attestation", "verify", cached_download, "--repo",
described_class::HOMEBREW_CORE_REPO, "--format", "json")
.and_raise(ErrorDuringExecution.new(["foo"], status: 1))
.and_raise(ErrorDuringExecution.new(["foo"], status: fake_error_status))
expect do
described_class.check_attestation fake_bottle,
@ -73,9 +89,27 @@ RSpec.describe Homebrew::Attestation do
end.to raise_error(described_class::InvalidAttestationError)
end
it "raises when gh returns invalid JSON" do
it "raises auth error when gh subprocess fails with auth exit code" do
expect(GitHub::API).to receive(:credentials)
.and_return(fake_gh_creds)
expect(Utils).to receive(:safe_popen_read)
.with(fake_gh, "attestation", "verify", cached_download, "--repo",
.with({ "GH_TOKEN" => fake_gh_creds }, fake_gh, "attestation", "verify", cached_download, "--repo",
described_class::HOMEBREW_CORE_REPO, "--format", "json")
.and_raise(ErrorDuringExecution.new(["foo"], status: fake_auth_status))
expect do
described_class.check_attestation fake_bottle,
described_class::HOMEBREW_CORE_REPO
end.to raise_error(described_class::GhAuthNeeded)
end
it "raises when gh returns invalid JSON" do
expect(GitHub::API).to receive(:credentials)
.and_return(fake_gh_creds)
expect(Utils).to receive(:safe_popen_read)
.with({ "GH_TOKEN" => fake_gh_creds }, fake_gh, "attestation", "verify", cached_download, "--repo",
described_class::HOMEBREW_CORE_REPO, "--format", "json")
.and_return("\"invalid json")
@ -86,8 +120,11 @@ RSpec.describe Homebrew::Attestation do
end
it "raises when gh returns other subjects" do
expect(GitHub::API).to receive(:credentials)
.and_return(fake_gh_creds)
expect(Utils).to receive(:safe_popen_read)
.with(fake_gh, "attestation", "verify", cached_download, "--repo",
.with({ "GH_TOKEN" => fake_gh_creds }, fake_gh, "attestation", "verify", cached_download, "--repo",
described_class::HOMEBREW_CORE_REPO, "--format", "json")
.and_return(fake_json_resp_wrong_sub)
@ -102,11 +139,14 @@ RSpec.describe Homebrew::Attestation do
before do
allow(described_class).to receive(:gh_executable)
.and_return(fake_gh)
allow(GitHub::API).to receive(:credentials)
.and_return(fake_gh_creds)
end
it "calls gh with args for homebrew-core" do
expect(Utils).to receive(:safe_popen_read)
.with(fake_gh, "attestation", "verify", cached_download, "--repo",
.with({ "GH_TOKEN" => fake_gh_creds }, fake_gh, "attestation", "verify", cached_download, "--repo",
described_class::HOMEBREW_CORE_REPO, "--format", "json", "--cert-identity",
described_class::HOMEBREW_CORE_CI_URI)
.and_return(fake_json_resp)
@ -116,14 +156,14 @@ RSpec.describe Homebrew::Attestation do
it "calls gh with args for backfill when homebrew-core fails" do
expect(Utils).to receive(:safe_popen_read)
.with(fake_gh, "attestation", "verify", cached_download, "--repo",
.with({ "GH_TOKEN" => fake_gh_creds }, fake_gh, "attestation", "verify", cached_download, "--repo",
described_class::HOMEBREW_CORE_REPO, "--format", "json", "--cert-identity",
described_class::HOMEBREW_CORE_CI_URI)
.once
.and_raise(described_class::InvalidAttestationError)
expect(Utils).to receive(:safe_popen_read)
.with(fake_gh, "attestation", "verify", cached_download, "--repo",
.with({ "GH_TOKEN" => fake_gh_creds }, fake_gh, "attestation", "verify", cached_download, "--repo",
described_class::BACKFILL_REPO, "--format", "json")
.and_return(fake_json_resp_backfill)
@ -132,14 +172,14 @@ RSpec.describe Homebrew::Attestation do
it "raises when the backfilled attestation is too new" do
expect(Utils).to receive(:safe_popen_read)
.with(fake_gh, "attestation", "verify", cached_download, "--repo",
.with({ "GH_TOKEN" => fake_gh_creds }, fake_gh, "attestation", "verify", cached_download, "--repo",
described_class::HOMEBREW_CORE_REPO, "--format", "json", "--cert-identity",
described_class::HOMEBREW_CORE_CI_URI)
.once
.and_raise(described_class::InvalidAttestationError)
expect(Utils).to receive(:safe_popen_read)
.with(fake_gh, "attestation", "verify", cached_download, "--repo",
.with({ "GH_TOKEN" => fake_gh_creds }, fake_gh, "attestation", "verify", cached_download, "--repo",
described_class::BACKFILL_REPO, "--format", "json")
.and_return(fake_json_resp_too_new)