Merge pull request #17049 from trail-of-forks/ww/attestation
attestation: add initial attestation helpers, integrate into `brew install`
This commit is contained in:
		
						commit
						c683e011b8
					
				
							
								
								
									
										138
									
								
								Library/Homebrew/attestation.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										138
									
								
								Library/Homebrew/attestation.rb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,138 @@
 | 
			
		||||
# typed: strict
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
require "date"
 | 
			
		||||
require "json"
 | 
			
		||||
require "utils/popen"
 | 
			
		||||
require "exceptions"
 | 
			
		||||
 | 
			
		||||
module Homebrew
 | 
			
		||||
  module Attestation
 | 
			
		||||
    # @api private
 | 
			
		||||
    HOMEBREW_CORE_REPO = "Homebrew/homebrew-core"
 | 
			
		||||
    # @api private
 | 
			
		||||
    HOMEBREW_CORE_CI_URI = "https://github.com/Homebrew/homebrew-core/.github/workflows/publish-commit-bottles.yml@refs/heads/master"
 | 
			
		||||
 | 
			
		||||
    # @api private
 | 
			
		||||
    BACKFILL_REPO = "trailofbits/homebrew-brew-verify"
 | 
			
		||||
    # @api private
 | 
			
		||||
    BACKFILL_REPO_CI_URI = "https://github.com/trailofbits/homebrew-brew-verify/.github/workflows/backfill_signatures.yml@refs/heads/main"
 | 
			
		||||
 | 
			
		||||
    # No backfill attestations after this date are considered valid.
 | 
			
		||||
    #
 | 
			
		||||
    # This date is shortly after the backfill operation for homebrew-core
 | 
			
		||||
    # completed, as can be seen here: <https://github.com/trailofbits/homebrew-brew-verify/attestations>.
 | 
			
		||||
    #
 | 
			
		||||
    # In effect, this means that, even if an attacker is able to compromise the backfill
 | 
			
		||||
    # signing workflow, they will be unable to convince a verifier to accept their newer,
 | 
			
		||||
    # malicious backfilled signatures.
 | 
			
		||||
    #
 | 
			
		||||
    # @api private
 | 
			
		||||
    BACKFILL_CUTOFF = T.let(DateTime.new(2024, 3, 14).freeze, DateTime)
 | 
			
		||||
 | 
			
		||||
    # Raised when attestation verification fails.
 | 
			
		||||
    #
 | 
			
		||||
    # @api private
 | 
			
		||||
    class InvalidAttestationError < RuntimeError; end
 | 
			
		||||
 | 
			
		||||
    # Returns a path to a suitable `gh` executable for attestation verification.
 | 
			
		||||
    #
 | 
			
		||||
    # @api private
 | 
			
		||||
    sig { returns(Pathname) }
 | 
			
		||||
    def self.gh_executable
 | 
			
		||||
      # NOTE: We disable HOMEBREW_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_VERIFY_ATTESTATIONS" => nil) do
 | 
			
		||||
        ensure_executable!("gh")
 | 
			
		||||
      end, T.nilable(Pathname))
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    # Verifies the given bottle against a cryptographic attestation of build provenance.
 | 
			
		||||
    #
 | 
			
		||||
    # The provenance is verified as originating from `signing_repo`, which is a `String`
 | 
			
		||||
    # that should be formatted as a GitHub `owner/repo`.
 | 
			
		||||
    #
 | 
			
		||||
    # Callers may additionally pass in `signing_workflow`, which will scope the attestation
 | 
			
		||||
    # down to an exact GitHub Actions workflow, in
 | 
			
		||||
    # `https://github/OWNER/REPO/.github/workflows/WORKFLOW.yml@REF` format.
 | 
			
		||||
    #
 | 
			
		||||
    # @return [Hash] the JSON-decoded response.
 | 
			
		||||
    # @raise [InvalidAttestationError] on any verification failures
 | 
			
		||||
    #
 | 
			
		||||
    # @api private
 | 
			
		||||
    sig {
 | 
			
		||||
      params(bottle: Bottle, signing_repo: String,
 | 
			
		||||
             signing_workflow: T.nilable(String), subject: T.nilable(String)).returns(T::Hash[T.untyped, T.untyped])
 | 
			
		||||
    }
 | 
			
		||||
    def self.check_attestation(bottle, signing_repo, signing_workflow = nil, subject = nil)
 | 
			
		||||
      cmd = [gh_executable, "attestation", "verify", bottle.cached_download, "--repo", signing_repo, "--format",
 | 
			
		||||
             "json"]
 | 
			
		||||
 | 
			
		||||
      cmd += ["--cert-identity", signing_workflow] if signing_workflow.present?
 | 
			
		||||
 | 
			
		||||
      begin
 | 
			
		||||
        output = Utils.safe_popen_read(*cmd)
 | 
			
		||||
      rescue ErrorDuringExecution => e
 | 
			
		||||
        raise InvalidAttestationError, "attestation verification failed: #{e}"
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      begin
 | 
			
		||||
        attestations = JSON.parse(output)
 | 
			
		||||
      rescue JSON::ParserError
 | 
			
		||||
        raise InvalidAttestationError, "attestation verification returned malformed JSON"
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      # `gh attestation verify` returns a JSON array of one or more results,
 | 
			
		||||
      # for all attestations that match the input's digest. We want to additionally
 | 
			
		||||
      # filter these down to just the attestation whose subject matches the bottle's name.
 | 
			
		||||
      subject = bottle.filename.to_s if subject.blank?
 | 
			
		||||
      attestation = attestations.find do |a|
 | 
			
		||||
        a.dig("verificationResult", "statement", "subject", 0, "name") == subject
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      raise InvalidAttestationError, "no attestation matches subject" if attestation.blank?
 | 
			
		||||
 | 
			
		||||
      attestation
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    # Verifies the given bottle against a cryptographic attestation of build provenance
 | 
			
		||||
    # from homebrew-core's CI, falling back on a "backfill" attestation for older bottles.
 | 
			
		||||
    #
 | 
			
		||||
    # This is a specialization of `check_attestation` for homebrew-core.
 | 
			
		||||
    #
 | 
			
		||||
    # @return [Hash] the JSON-decoded response
 | 
			
		||||
    # @raise [InvalidAttestationError] on any verification failures
 | 
			
		||||
    #
 | 
			
		||||
    # @api private
 | 
			
		||||
    sig { params(bottle: Bottle).returns(T::Hash[T.untyped, T.untyped]) }
 | 
			
		||||
    def self.check_core_attestation(bottle)
 | 
			
		||||
      begin
 | 
			
		||||
        attestation = check_attestation bottle, HOMEBREW_CORE_REPO, HOMEBREW_CORE_CI_URI
 | 
			
		||||
        return attestation
 | 
			
		||||
      rescue InvalidAttestationError
 | 
			
		||||
        odebug "falling back on backfilled attestation for #{bottle}"
 | 
			
		||||
 | 
			
		||||
        # Our backfilled attestation is a little unique: the subject is not just the bottle
 | 
			
		||||
        # filename, but also has the bottle's hosted URL hash prepended to it.
 | 
			
		||||
        # This was originally unintentional, but has a virtuous side effect of further
 | 
			
		||||
        # limiting domain separation on the backfilled signatures (by committing them to
 | 
			
		||||
        # their original bottle URLs).
 | 
			
		||||
        url_sha256 = Digest::SHA256.hexdigest(bottle.url)
 | 
			
		||||
        subject = "#{url_sha256}--#{bottle.filename}"
 | 
			
		||||
 | 
			
		||||
        backfill_attestation = check_attestation bottle, BACKFILL_REPO, BACKFILL_REPO_CI_URI, subject
 | 
			
		||||
        timestamp = backfill_attestation.dig("verificationResult", "verifiedTimestamps",
 | 
			
		||||
                                             0, "timestamp")
 | 
			
		||||
 | 
			
		||||
        raise InvalidAttestationError, "backfill attestation is missing verified timestamp" if timestamp.nil?
 | 
			
		||||
 | 
			
		||||
        if DateTime.parse(timestamp) > BACKFILL_CUTOFF
 | 
			
		||||
          raise InvalidAttestationError, "backfill attestation post-dates cutoff"
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      backfill_attestation
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@ -422,6 +422,11 @@ module Homebrew
 | 
			
		||||
                     "useful to avoid long-running Homebrew commands being killed due to no output.",
 | 
			
		||||
        boolean:     true,
 | 
			
		||||
      },
 | 
			
		||||
      HOMEBREW_VERIFY_ATTESTATIONS:              {
 | 
			
		||||
        description: "If set, Homebrew will use the `gh` tool to verify cryptographic attestations " \
 | 
			
		||||
                     "of build provenance for bottles from homebrew-core.",
 | 
			
		||||
        boolean:     true,
 | 
			
		||||
      },
 | 
			
		||||
      SUDO_ASKPASS:                              {
 | 
			
		||||
        description: "If set, pass the `-A` option when calling `sudo`(8).",
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
@ -22,6 +22,7 @@ require "utils/spdx"
 | 
			
		||||
require "deprecate_disable"
 | 
			
		||||
require "unlink"
 | 
			
		||||
require "service"
 | 
			
		||||
require "attestation"
 | 
			
		||||
 | 
			
		||||
# Installer for a formula.
 | 
			
		||||
#
 | 
			
		||||
@ -1256,6 +1257,25 @@ on_request: installed_on_request?, options:)
 | 
			
		||||
 | 
			
		||||
  sig { void }
 | 
			
		||||
  def pour
 | 
			
		||||
    if Homebrew::EnvConfig.verify_attestations? && formula.tap&.core_tap?
 | 
			
		||||
      ohai "Verifying attestation for #{formula.name}"
 | 
			
		||||
      begin
 | 
			
		||||
        Homebrew::Attestation.check_core_attestation formula.bottle
 | 
			
		||||
      rescue Homebrew::Attestation::InvalidAttestationError => e
 | 
			
		||||
        raise CannotInstallFormulaError, <<~EOS
 | 
			
		||||
          The bottle for #{formula.name} has an invalid build provenance attestation.
 | 
			
		||||
 | 
			
		||||
          This may indicate that the bottle was not produced by the expected
 | 
			
		||||
          tap, or was maliciously inserted into the expected tap's bottle
 | 
			
		||||
          storage.
 | 
			
		||||
 | 
			
		||||
          Additional context:
 | 
			
		||||
 | 
			
		||||
          #{e}
 | 
			
		||||
        EOS
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    HOMEBREW_CELLAR.cd do
 | 
			
		||||
      downloader.stage
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
@ -423,6 +423,11 @@ class Bottle
 | 
			
		||||
    github_packages_manifest_resource_tab(github_packages_manifest_resource)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  sig { returns(Filename) }
 | 
			
		||||
  def filename
 | 
			
		||||
    Filename.create(resource.owner, @tag, @spec.rebuild)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def github_packages_manifest_resource_tab(github_packages_manifest_resource)
 | 
			
		||||
 | 
			
		||||
@ -260,5 +260,8 @@ module Homebrew::EnvConfig
 | 
			
		||||
 | 
			
		||||
    sig { returns(T::Boolean) }
 | 
			
		||||
    def verbose_using_dots?; end
 | 
			
		||||
 | 
			
		||||
    sig { returns(T::Boolean) }
 | 
			
		||||
    def verify_attestations?; end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										153
									
								
								Library/Homebrew/test/attestation_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										153
									
								
								Library/Homebrew/test/attestation_spec.rb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,153 @@
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
require "diagnostic"
 | 
			
		||||
 | 
			
		||||
RSpec.describe Homebrew::Attestation do
 | 
			
		||||
  let(:fake_gh) { Pathname.new("/extremely/fake/gh") }
 | 
			
		||||
  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}" }
 | 
			
		||||
  let(:fake_bottle) do
 | 
			
		||||
    instance_double(Bottle, cached_download:, filename: fake_bottle_filename, url: fake_bottle_url)
 | 
			
		||||
  end
 | 
			
		||||
  let(:fake_json_resp) do
 | 
			
		||||
    JSON.dump([
 | 
			
		||||
      { verificationResult: {
 | 
			
		||||
        verifiedTimestamps: [{ timestamp: "2024-03-13T00:00:00Z" }],
 | 
			
		||||
        statement:          { subject: [{ name: fake_bottle_filename.to_s }] },
 | 
			
		||||
      } },
 | 
			
		||||
    ])
 | 
			
		||||
  end
 | 
			
		||||
  let(:fake_json_resp_backfill) do
 | 
			
		||||
    JSON.dump([
 | 
			
		||||
      { verificationResult: {
 | 
			
		||||
        verifiedTimestamps: [{ timestamp: "2024-03-13T00:00:00Z" }],
 | 
			
		||||
        statement:          {
 | 
			
		||||
          subject: [{ name: "#{Digest::SHA256.hexdigest(fake_bottle_url)}--#{fake_bottle_filename}" }],
 | 
			
		||||
        },
 | 
			
		||||
      } },
 | 
			
		||||
    ])
 | 
			
		||||
  end
 | 
			
		||||
  let(:fake_json_resp_too_new) do
 | 
			
		||||
    JSON.dump([
 | 
			
		||||
      { verificationResult: {
 | 
			
		||||
        verifiedTimestamps: [{ timestamp: "2024-03-15T00:00:00Z" }],
 | 
			
		||||
        statement:          { subject: [{ name: fake_bottle_filename.to_s }] },
 | 
			
		||||
      } },
 | 
			
		||||
    ])
 | 
			
		||||
  end
 | 
			
		||||
  let(:fake_json_resp_wrong_sub) do
 | 
			
		||||
    JSON.dump([
 | 
			
		||||
      { verificationResult: {
 | 
			
		||||
        verifiedTimestamps: [{ timestamp: "2024-03-13T00:00:00Z" }],
 | 
			
		||||
        statement:          { subject: [{ name: "wrong-subject.tar.gz" }] },
 | 
			
		||||
      } },
 | 
			
		||||
    ])
 | 
			
		||||
  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" do
 | 
			
		||||
    before do
 | 
			
		||||
      allow(described_class).to receive(:gh_executable)
 | 
			
		||||
        .and_return(fake_gh)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it "raises when gh subprocess fails" do
 | 
			
		||||
      expect(Utils).to receive(:safe_popen_read)
 | 
			
		||||
        .with(fake_gh, "attestation", "verify", cached_download, "--repo",
 | 
			
		||||
              described_class::HOMEBREW_CORE_REPO, "--format", "json")
 | 
			
		||||
        .and_raise(ErrorDuringExecution.new(["foo"], status: 1))
 | 
			
		||||
 | 
			
		||||
      expect do
 | 
			
		||||
        described_class.check_attestation fake_bottle,
 | 
			
		||||
                                          described_class::HOMEBREW_CORE_REPO
 | 
			
		||||
      end.to raise_error(described_class::InvalidAttestationError)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it "raises when gh returns invalid JSON" do
 | 
			
		||||
      expect(Utils).to receive(:safe_popen_read)
 | 
			
		||||
        .with(fake_gh, "attestation", "verify", cached_download, "--repo",
 | 
			
		||||
              described_class::HOMEBREW_CORE_REPO, "--format", "json")
 | 
			
		||||
        .and_return("\"invalid json")
 | 
			
		||||
 | 
			
		||||
      expect do
 | 
			
		||||
        described_class.check_attestation fake_bottle,
 | 
			
		||||
                                          described_class::HOMEBREW_CORE_REPO
 | 
			
		||||
      end.to raise_error(described_class::InvalidAttestationError)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it "raises when gh returns other subjects" do
 | 
			
		||||
      expect(Utils).to receive(:safe_popen_read)
 | 
			
		||||
        .with(fake_gh, "attestation", "verify", cached_download, "--repo",
 | 
			
		||||
              described_class::HOMEBREW_CORE_REPO, "--format", "json")
 | 
			
		||||
        .and_return(fake_json_resp_wrong_sub)
 | 
			
		||||
 | 
			
		||||
      expect do
 | 
			
		||||
        described_class.check_attestation fake_bottle,
 | 
			
		||||
                                          described_class::HOMEBREW_CORE_REPO
 | 
			
		||||
      end.to raise_error(described_class::InvalidAttestationError)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe "::check_core_attestation" do
 | 
			
		||||
    before do
 | 
			
		||||
      allow(described_class).to receive(:gh_executable)
 | 
			
		||||
        .and_return(fake_gh)
 | 
			
		||||
    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",
 | 
			
		||||
              described_class::HOMEBREW_CORE_REPO, "--format", "json", "--cert-identity",
 | 
			
		||||
              described_class::HOMEBREW_CORE_CI_URI)
 | 
			
		||||
        .and_return(fake_json_resp)
 | 
			
		||||
 | 
			
		||||
      described_class.check_core_attestation fake_bottle
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    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",
 | 
			
		||||
              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",
 | 
			
		||||
              described_class::BACKFILL_REPO, "--format", "json", "--cert-identity",
 | 
			
		||||
              described_class::BACKFILL_REPO_CI_URI)
 | 
			
		||||
        .and_return(fake_json_resp_backfill)
 | 
			
		||||
 | 
			
		||||
      described_class.check_core_attestation fake_bottle
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    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",
 | 
			
		||||
              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",
 | 
			
		||||
              described_class::BACKFILL_REPO, "--format", "json", "--cert-identity",
 | 
			
		||||
              described_class::BACKFILL_REPO_CI_URI)
 | 
			
		||||
        .and_return(fake_json_resp_too_new)
 | 
			
		||||
 | 
			
		||||
      expect do
 | 
			
		||||
        described_class.check_core_attestation fake_bottle
 | 
			
		||||
      end.to raise_error(described_class::InvalidAttestationError)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@ -1,62 +1,17 @@
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
require "software_spec"
 | 
			
		||||
require "test/support/fixtures/testball_bottle"
 | 
			
		||||
 | 
			
		||||
RSpec.describe BottleSpecification do
 | 
			
		||||
  subject(:bottle_spec) { described_class.new }
 | 
			
		||||
RSpec.describe Bottle do
 | 
			
		||||
  describe "#filename" do
 | 
			
		||||
    it "renders the bottle filename" do
 | 
			
		||||
      bottle_spec = BottleSpecification.new
 | 
			
		||||
      bottle_spec.sha256(arm64_big_sur: "deadbeef" * 8)
 | 
			
		||||
      tag = Utils::Bottles::Tag.from_symbol :arm64_big_sur
 | 
			
		||||
      bottle = described_class.new(TestballBottle.new, bottle_spec, tag)
 | 
			
		||||
 | 
			
		||||
  describe "#sha256" do
 | 
			
		||||
    it "works without cellar" do
 | 
			
		||||
      checksums = {
 | 
			
		||||
        arm64_big_sur: "deadbeef" * 8,
 | 
			
		||||
        big_sur:       "faceb00c" * 8,
 | 
			
		||||
        catalina:      "baadf00d" * 8,
 | 
			
		||||
        mojave:        "8badf00d" * 8,
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      checksums.each_pair do |cat, digest|
 | 
			
		||||
        bottle_spec.sha256(cat => digest)
 | 
			
		||||
        tag_spec = bottle_spec.tag_specification_for(Utils::Bottles::Tag.from_symbol(cat))
 | 
			
		||||
        expect(Checksum.new(digest)).to eq(tag_spec.checksum)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it "works with cellar" do
 | 
			
		||||
      checksums = [
 | 
			
		||||
        { cellar: :any_skip_relocation, tag: :arm64_big_sur,  digest: "deadbeef" * 8 },
 | 
			
		||||
        { cellar: :any, tag: :big_sur, digest: "faceb00c" * 8 },
 | 
			
		||||
        { cellar: "/usr/local/Cellar", tag: :catalina, digest: "baadf00d" * 8 },
 | 
			
		||||
        { cellar: Homebrew::DEFAULT_CELLAR, tag: :mojave, digest: "8badf00d" * 8 },
 | 
			
		||||
      ]
 | 
			
		||||
 | 
			
		||||
      checksums.each do |checksum|
 | 
			
		||||
        bottle_spec.sha256(cellar: checksum[:cellar], checksum[:tag] => checksum[:digest])
 | 
			
		||||
        tag_spec = bottle_spec.tag_specification_for(Utils::Bottles::Tag.from_symbol(checksum[:tag]))
 | 
			
		||||
        expect(Checksum.new(checksum[:digest])).to eq(tag_spec.checksum)
 | 
			
		||||
        expect(checksum[:tag]).to eq(tag_spec.tag.to_sym)
 | 
			
		||||
        checksum[:cellar] ||= Homebrew::DEFAULT_CELLAR
 | 
			
		||||
        expect(checksum[:cellar]).to eq(tag_spec.cellar)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe "#compatible_locations?" do
 | 
			
		||||
    it "checks if the bottle cellar is relocatable" do
 | 
			
		||||
      expect(bottle_spec.compatible_locations?).to be false
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe "#tag_to_cellar" do
 | 
			
		||||
    it "returns the cellar for a tag" do
 | 
			
		||||
      expect(bottle_spec.tag_to_cellar).to eq Utils::Bottles.tag.default_cellar
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  %w[root_url rebuild].each do |method|
 | 
			
		||||
    specify "##{method}" do
 | 
			
		||||
      object = Object.new
 | 
			
		||||
      bottle_spec.public_send(method, object)
 | 
			
		||||
      expect(bottle_spec.public_send(method)).to eq(object)
 | 
			
		||||
      expect(bottle.filename.to_s).to eq("testball_bottle--0.1.arm64_big_sur.bottle.tar.gz")
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,62 @@
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
require "software_spec"
 | 
			
		||||
 | 
			
		||||
RSpec.describe BottleSpecification do
 | 
			
		||||
  subject(:bottle_spec) { described_class.new }
 | 
			
		||||
 | 
			
		||||
  describe "#sha256" do
 | 
			
		||||
    it "works without cellar" do
 | 
			
		||||
      checksums = {
 | 
			
		||||
        arm64_big_sur: "deadbeef" * 8,
 | 
			
		||||
        big_sur:       "faceb00c" * 8,
 | 
			
		||||
        catalina:      "baadf00d" * 8,
 | 
			
		||||
        mojave:        "8badf00d" * 8,
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      checksums.each_pair do |cat, digest|
 | 
			
		||||
        bottle_spec.sha256(cat => digest)
 | 
			
		||||
        tag_spec = bottle_spec.tag_specification_for(Utils::Bottles::Tag.from_symbol(cat))
 | 
			
		||||
        expect(Checksum.new(digest)).to eq(tag_spec.checksum)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it "works with cellar" do
 | 
			
		||||
      checksums = [
 | 
			
		||||
        { cellar: :any_skip_relocation, tag: :arm64_big_sur,  digest: "deadbeef" * 8 },
 | 
			
		||||
        { cellar: :any, tag: :big_sur, digest: "faceb00c" * 8 },
 | 
			
		||||
        { cellar: "/usr/local/Cellar", tag: :catalina, digest: "baadf00d" * 8 },
 | 
			
		||||
        { cellar: Homebrew::DEFAULT_CELLAR, tag: :mojave, digest: "8badf00d" * 8 },
 | 
			
		||||
      ]
 | 
			
		||||
 | 
			
		||||
      checksums.each do |checksum|
 | 
			
		||||
        bottle_spec.sha256(cellar: checksum[:cellar], checksum[:tag] => checksum[:digest])
 | 
			
		||||
        tag_spec = bottle_spec.tag_specification_for(Utils::Bottles::Tag.from_symbol(checksum[:tag]))
 | 
			
		||||
        expect(Checksum.new(checksum[:digest])).to eq(tag_spec.checksum)
 | 
			
		||||
        expect(checksum[:tag]).to eq(tag_spec.tag.to_sym)
 | 
			
		||||
        checksum[:cellar] ||= Homebrew::DEFAULT_CELLAR
 | 
			
		||||
        expect(checksum[:cellar]).to eq(tag_spec.cellar)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe "#compatible_locations?" do
 | 
			
		||||
    it "checks if the bottle cellar is relocatable" do
 | 
			
		||||
      expect(bottle_spec.compatible_locations?).to be false
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe "#tag_to_cellar" do
 | 
			
		||||
    it "returns the cellar for a tag" do
 | 
			
		||||
      expect(bottle_spec.tag_to_cellar).to eq Utils::Bottles.tag.default_cellar
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  %w[root_url rebuild].each do |method|
 | 
			
		||||
    specify "##{method}" do
 | 
			
		||||
      object = Object.new
 | 
			
		||||
      bottle_spec.public_send(method, object)
 | 
			
		||||
      expect(bottle_spec.public_send(method)).to eq(object)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user