diff --git a/Library/Homebrew/attestation.rb b/Library/Homebrew/attestation.rb index 0ab000e583c6b..7dd23bdad4fb1 100644 --- a/Library/Homebrew/attestation.rb +++ b/Library/Homebrew/attestation.rb @@ -4,6 +4,7 @@ require "date" require "json" require "utils/popen" +require "utils/github/api" require "exceptions" require "system_command" @@ -52,7 +53,6 @@ def self.enabled? return true if Homebrew::EnvConfig.verify_attestations? return false if GitHub::API.credentials.blank? return false if ENV.fetch("CI", false) - return false unless Formula["gh"].any_version_installed? Homebrew::EnvConfig.developer? || Homebrew::EnvConfig.devcmdrun? end @@ -65,9 +65,25 @@ def self.gh_executable # NOTE: We set HOMEBREW_NO_VERIFY_ATTESTATIONS when installing `gh` itself, # to prevent a cycle during bootstrapping. This can eventually be resolved # by vendoring a pure-Ruby Sigstore verifier client. - @gh_executable ||= T.let(with_env(HOMEBREW_NO_VERIFY_ATTESTATIONS: "1") do - ensure_executable!("gh") - end, T.nilable(Pathname)) + @gh_executable ||= T.let(nil, T.nilable(Pathname)) + return @gh_executable if @gh_executable.present? + + with_env(HOMEBREW_NO_VERIFY_ATTESTATIONS: "1") do + @gh_executable = ensure_executable!("gh", reason: "verifying attestations") + + gh_version = Version.new(system_command!(@gh_executable, args: ["--version"], print_stderr: false) + .stdout.match(/\d+(?:\.\d+)+/i).to_s) + if gh_version < GH_ATTESTATION_MIN_VERSION + if Formula["gh"].version < GH_ATTESTATION_MIN_VERSION + raise "#{@gh_executable} is too old, you must upgrade it to >=#{GH_ATTESTATION_MIN_VERSION} to continue" + end + + @gh_executable = ensure_formula_installed!("gh", latest: true, + reason: "verifying attestations").opt_bin/"gh" + end + end + + T.must(@gh_executable) end # Verifies the given bottle against a cryptographic attestation of build provenance. @@ -107,13 +123,6 @@ def self.check_attestation(bottle, signing_repo, signing_workflow = nil, subject # Even if we have credentials, they may be invalid or malformed. raise GhAuthNeeded, "invalid credentials" if e.status.exitstatus == 4 - gh_version = Version.new(system_command!(gh_executable, args: ["--version"], print_stderr: false) - .stdout.match(/\d+(?:\.\d+)+/i).to_s) - if gh_version < GH_ATTESTATION_MIN_VERSION - raise e, - "#{gh_executable} is too old, you must upgrade it to continue" - end - raise InvalidAttestationError, "attestation verification failed: #{e}" end diff --git a/Library/Homebrew/cmd/install.rb b/Library/Homebrew/cmd/install.rb index 763a933606198..9b8b9d59e4c45 100644 --- a/Library/Homebrew/cmd/install.rb +++ b/Library/Homebrew/cmd/install.rb @@ -263,6 +263,14 @@ def run end end + if Homebrew::Attestation.enabled? + if formulae.include?(Formula["gh"]) + formulae.unshift(T.must(formulae.delete(Formula["gh"]))) + else + Homebrew::Attestation.gh_executable + end + end + # if the user's flags will prevent bottle only-installations when no # developer tools are available, we need to stop them early on build_flags = [] diff --git a/Library/Homebrew/cmd/reinstall.rb b/Library/Homebrew/cmd/reinstall.rb index 20f87915f937b..5f8a75144d55e 100644 --- a/Library/Homebrew/cmd/reinstall.rb +++ b/Library/Homebrew/cmd/reinstall.rb @@ -124,6 +124,14 @@ def run end end + if Homebrew::Attestation.enabled? + if formulae.include?(Formula["gh"]) + formulae.unshift(T.must(formulae.delete(Formula["gh"]))) + else + Homebrew::Attestation.gh_executable + end + end + Install.perform_preinstall_checks formulae.each do |formula| diff --git a/Library/Homebrew/cmd/upgrade.rb b/Library/Homebrew/cmd/upgrade.rb index aab7e7714da04..cab4f27556c2e 100644 --- a/Library/Homebrew/cmd/upgrade.rb +++ b/Library/Homebrew/cmd/upgrade.rb @@ -134,6 +134,14 @@ def run only_upgrade_formulae = formulae.present? && casks.blank? only_upgrade_casks = casks.present? && formulae.blank? + if Homebrew::Attestation.enabled? + if formulae.include?(Formula["gh"]) + formulae.unshift(formulae.delete(Formula["gh"])) + else + Homebrew::Attestation.gh_executable + end + end + upgrade_outdated_formulae(formulae) unless only_upgrade_casks upgrade_outdated_casks(casks) unless only_upgrade_formulae diff --git a/Library/Homebrew/test/attestation_spec.rb b/Library/Homebrew/test/attestation_spec.rb index 9011b25579588..b82dc1156563a 100644 --- a/Library/Homebrew/test/attestation_spec.rb +++ b/Library/Homebrew/test/attestation_spec.rb @@ -6,6 +6,7 @@ let(:fake_gh) { Pathname.new("/extremely/fake/gh") } let(:fake_old_gh) { Pathname.new("/extremely/fake/old/gh") } let(:fake_gh_creds) { "fake-gh-api-token" } + let(:fake_gh_formula) { instance_double(Formula, "gh", opt_bin: Pathname.new("/extremely/fake")) } let(:fake_gh_version) { instance_double(SystemCommand::Result, stdout: "2.49.0") } let(:fake_old_gh_version) { instance_double(SystemCommand::Result, stdout: "2.48.0") } let(:fake_error_status) { instance_double(Process::Status, exitstatus: 1, termsig: nil) } @@ -68,40 +69,26 @@ end describe "::gh_executable" do - it "calls ensure_executable" do - expect(described_class).to receive(:ensure_executable!) - .with("gh") - .and_return(fake_gh) - - described_class.gh_executable - end - end - - describe "::check_attestation fails with old gh" do before do - allow(described_class).to receive(:gh_executable) - .and_return(fake_old_gh) + allow(Formulary).to receive(:factory) + .with("gh") + .and_return(instance_double(Formula, version: Version.new("2.49.0"))) allow(described_class).to receive(:system_command!) .with(fake_old_gh, args: ["--version"], print_stderr: false) .and_return(fake_old_gh_version) end - it "raises when gh is too old" do - expect(GitHub::API).to receive(:credentials) - .and_return(fake_gh_creds) + it "calls ensure_executable and ensure_formula_installed" do + expect(described_class).to receive(:ensure_executable!) + .with("gh", reason: "verifying attestations") + .and_return(fake_old_gh) - expect(described_class).to receive(:system_command!) - .with(fake_old_gh, args: ["attestation", "verify", cached_download, "--repo", - described_class::HOMEBREW_CORE_REPO, "--format", "json"], - env: { "GH_TOKEN" => fake_gh_creds }, secrets: [fake_gh_creds], - print_stderr: false, chdir: HOMEBREW_TEMP) - .and_raise(ErrorDuringExecution.new(["foo"], status: fake_error_status)) + expect(described_class).to receive(:ensure_formula_installed!) + .with("gh", latest: true, reason: "verifying attestations") + .and_return(fake_gh_formula) - expect do - described_class.check_attestation fake_bottle, - described_class::HOMEBREW_CORE_REPO - end.to raise_error(ErrorDuringExecution) + described_class.gh_executable end end @@ -109,10 +96,6 @@ before do allow(described_class).to receive(:gh_executable) .and_return(fake_gh) - - allow(described_class).to receive(:system_command!) - .with(fake_gh, args: ["--version"], print_stderr: false) - .and_return(fake_gh_version) end it "raises without any gh credentials" do