From 16d547b030fd8acc048d6789e734a19ca5b28b56 Mon Sep 17 00:00:00 2001 From: Bo Anderson Date: Thu, 18 Jul 2024 16:11:25 +0100 Subject: [PATCH] attestation: handle bad configurations better --- Library/Homebrew/attestation.rb | 14 ++++++++++++-- Library/Homebrew/formula_installer.rb | 16 ++++++++++++++++ Library/Homebrew/test/attestation_spec.rb | 22 +++++++++++----------- 3 files changed, 39 insertions(+), 13 deletions(-) diff --git a/Library/Homebrew/attestation.rb b/Library/Homebrew/attestation.rb index 7dd23bdad4..0d7072b192 100644 --- a/Library/Homebrew/attestation.rb +++ b/Library/Homebrew/attestation.rb @@ -44,6 +44,12 @@ module Homebrew # @api private class GhAuthNeeded < RuntimeError; end + # Raised if attestation verification cannot continue due to invalid + # credentials. + # + # @api private + class GhAuthInvalid < RuntimeError; end + # Returns whether attestation verification is enabled. # # @api private @@ -53,6 +59,7 @@ module Homebrew return true if Homebrew::EnvConfig.verify_attestations? return false if GitHub::API.credentials.blank? return false if ENV.fetch("CI", false) + return false if OS.unsupported_configuration? Homebrew::EnvConfig.developer? || Homebrew::EnvConfig.devcmdrun? end @@ -117,11 +124,14 @@ module Homebrew raise GhAuthNeeded, "missing credentials" if credentials.blank? begin - result = system_command!(gh_executable, args: cmd, env: { "GH_TOKEN" => credentials }, + result = system_command!(gh_executable, args: cmd, + env: { "GH_TOKEN" => credentials, "GH_HOST" => "github.com" }, secrets: [credentials], print_stderr: false, chdir: HOMEBREW_TEMP) rescue ErrorDuringExecution => e # Even if we have credentials, they may be invalid or malformed. - raise GhAuthNeeded, "invalid credentials" if e.status.exitstatus == 4 + if e.status.exitstatus == 4 || e.stderr.include?("HTTP 401: Bad credentials") + raise GhAuthInvalid, "invalid credentials" + end raise InvalidAttestationError, "attestation verification failed: #{e}" end diff --git a/Library/Homebrew/formula_installer.rb b/Library/Homebrew/formula_installer.rb index 423d43a7a5..ee9d16981e 100644 --- a/Library/Homebrew/formula_installer.rb +++ b/Library/Homebrew/formula_installer.rb @@ -1284,6 +1284,22 @@ on_request: installed_on_request?, options:) ohai "Verifying attestation for #{formula.name}" begin Homebrew::Attestation.check_core_attestation formula.bottle + rescue Homebrew::Attestation::GhAuthInvalid + # Only raise an error if we explicitly opted-in to verification. + raise CannotInstallFormulaError, <<~EOS if Homebrew::EnvConfig.verify_attestations? + The bottle for #{formula.name} could not be verified. + + This typically indicates an invalid GitHub API token. + + If you have `HOMEBREW_GITHUB_API_TOKEN` set, check it is correct + or unset it and instead run: + + gh auth login + EOS + + # If we didn't explicitly opt-in, then quietly opt-out in the case of invalid credentials. + # Based on user reports, a significant number of users are running with stale tokens. + ENV["HOMEBREW_NO_VERIFY_ATTESTATIONS"] = "1" rescue Homebrew::Attestation::GhAuthNeeded raise CannotInstallFormulaError, <<~EOS The bottle for #{formula.name} could not be verified. diff --git a/Library/Homebrew/test/attestation_spec.rb b/Library/Homebrew/test/attestation_spec.rb index b82dc11565..6e8d528061 100644 --- a/Library/Homebrew/test/attestation_spec.rb +++ b/Library/Homebrew/test/attestation_spec.rb @@ -115,7 +115,7 @@ RSpec.describe Homebrew::Attestation do expect(described_class).to receive(:system_command!) .with(fake_gh, args: ["attestation", "verify", cached_download, "--repo", described_class::HOMEBREW_CORE_REPO, "--format", "json"], - env: { "GH_TOKEN" => fake_gh_creds }, secrets: [fake_gh_creds], + env: { "GH_TOKEN" => fake_gh_creds, "GH_HOST" => "github.com" }, secrets: [fake_gh_creds], print_stderr: false, chdir: HOMEBREW_TEMP) .and_raise(ErrorDuringExecution.new(["foo"], status: fake_error_status)) @@ -132,14 +132,14 @@ RSpec.describe Homebrew::Attestation do expect(described_class).to receive(:system_command!) .with(fake_gh, args: ["attestation", "verify", cached_download, "--repo", described_class::HOMEBREW_CORE_REPO, "--format", "json"], - env: { "GH_TOKEN" => fake_gh_creds }, secrets: [fake_gh_creds], + env: { "GH_TOKEN" => fake_gh_creds, "GH_HOST" => "github.com" }, secrets: [fake_gh_creds], print_stderr: false, chdir: HOMEBREW_TEMP) .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.to raise_error(described_class::GhAuthInvalid) end it "raises when gh returns invalid JSON" do @@ -149,7 +149,7 @@ RSpec.describe Homebrew::Attestation do expect(described_class).to receive(:system_command!) .with(fake_gh, args: ["attestation", "verify", cached_download, "--repo", described_class::HOMEBREW_CORE_REPO, "--format", "json"], - env: { "GH_TOKEN" => fake_gh_creds }, secrets: [fake_gh_creds], + env: { "GH_TOKEN" => fake_gh_creds, "GH_HOST" => "github.com" }, secrets: [fake_gh_creds], print_stderr: false, chdir: HOMEBREW_TEMP) .and_return(fake_result_invalid_json) @@ -166,7 +166,7 @@ RSpec.describe Homebrew::Attestation do expect(described_class).to receive(:system_command!) .with(fake_gh, args: ["attestation", "verify", cached_download, "--repo", described_class::HOMEBREW_CORE_REPO, "--format", "json"], - env: { "GH_TOKEN" => fake_gh_creds }, secrets: [fake_gh_creds], + env: { "GH_TOKEN" => fake_gh_creds, "GH_HOST" => "github.com" }, secrets: [fake_gh_creds], print_stderr: false, chdir: HOMEBREW_TEMP) .and_return(fake_json_resp_wrong_sub) @@ -183,7 +183,7 @@ RSpec.describe Homebrew::Attestation do expect(described_class).to receive(:system_command!) .with(fake_gh, args: ["attestation", "verify", cached_download, "--repo", described_class::HOMEBREW_CORE_REPO, "--format", "json"], - env: { "GH_TOKEN" => fake_gh_creds }, secrets: [fake_gh_creds], + env: { "GH_TOKEN" => fake_gh_creds, "GH_HOST" => "github.com" }, secrets: [fake_gh_creds], print_stderr: false, chdir: HOMEBREW_TEMP) .and_return(fake_result_json_resp) @@ -204,7 +204,7 @@ RSpec.describe Homebrew::Attestation do expect(described_class).to receive(:system_command!) .with(fake_gh, args: ["attestation", "verify", cached_download, "--repo", described_class::HOMEBREW_CORE_REPO, "--format", "json"], - env: { "GH_TOKEN" => fake_gh_creds }, secrets: [fake_gh_creds], + env: { "GH_TOKEN" => fake_gh_creds, "GH_HOST" => "github.com" }, secrets: [fake_gh_creds], print_stderr: false, chdir: HOMEBREW_TEMP) .and_return(fake_result_json_resp) @@ -215,7 +215,7 @@ RSpec.describe Homebrew::Attestation do expect(described_class).to receive(:system_command!) .with(fake_gh, args: ["attestation", "verify", cached_download, "--repo", described_class::HOMEBREW_CORE_REPO, "--format", "json"], - env: { "GH_TOKEN" => fake_gh_creds }, secrets: [fake_gh_creds], + env: { "GH_TOKEN" => fake_gh_creds, "GH_HOST" => "github.com" }, secrets: [fake_gh_creds], print_stderr: false, chdir: HOMEBREW_TEMP) .once .and_raise(described_class::InvalidAttestationError) @@ -223,7 +223,7 @@ RSpec.describe Homebrew::Attestation do expect(described_class).to receive(:system_command!) .with(fake_gh, args: ["attestation", "verify", cached_download, "--repo", described_class::BACKFILL_REPO, "--format", "json"], - env: { "GH_TOKEN" => fake_gh_creds }, secrets: [fake_gh_creds], + env: { "GH_TOKEN" => fake_gh_creds, "GH_HOST" => "github.com" }, secrets: [fake_gh_creds], print_stderr: false, chdir: HOMEBREW_TEMP) .and_return(fake_result_json_resp_backfill) @@ -234,7 +234,7 @@ RSpec.describe Homebrew::Attestation do expect(described_class).to receive(:system_command!) .with(fake_gh, args: ["attestation", "verify", cached_download, "--repo", described_class::HOMEBREW_CORE_REPO, "--format", "json"], - env: { "GH_TOKEN" => fake_gh_creds }, secrets: [fake_gh_creds], + env: { "GH_TOKEN" => fake_gh_creds, "GH_HOST" => "github.com" }, secrets: [fake_gh_creds], print_stderr: false, chdir: HOMEBREW_TEMP) .once .and_raise(described_class::InvalidAttestationError) @@ -242,7 +242,7 @@ RSpec.describe Homebrew::Attestation do expect(described_class).to receive(:system_command!) .with(fake_gh, args: ["attestation", "verify", cached_download, "--repo", described_class::BACKFILL_REPO, "--format", "json"], - env: { "GH_TOKEN" => fake_gh_creds }, secrets: [fake_gh_creds], + env: { "GH_TOKEN" => fake_gh_creds, "GH_HOST" => "github.com" }, secrets: [fake_gh_creds], print_stderr: false, chdir: HOMEBREW_TEMP) .and_return(fake_result_json_resp_too_new)