Module: Homebrew::Attestation Private

Defined in:
attestation.rb

This module is part of a private API. This module may only be used in the Homebrew/brew repository. Third parties should avoid using this module if possible, as it may be removed or changed without warning.

Defined Under Namespace

Classes: InvalidAttestationError

Constant Summary collapse

HOMEBREW_CORE_REPO =

This constant is part of a private API. This constant may only be used in the Homebrew/brew repository. Third parties should avoid using this constant if possible, as it may be removed or changed without warning.

"Homebrew/homebrew-core"
HOMEBREW_CORE_CI_URI =

This constant is part of a private API. This constant may only be used in the Homebrew/brew repository. Third parties should avoid using this constant if possible, as it may be removed or changed without warning.

"https://github.com/Homebrew/homebrew-core/.github/workflows/publish-commit-bottles.yml@refs/heads/master"
BACKFILL_REPO =

This constant is part of a private API. This constant may only be used in the Homebrew/brew repository. Third parties should avoid using this constant if possible, as it may be removed or changed without warning.

"trailofbits/homebrew-brew-verify"
BACKFILL_CUTOFF =

This constant is part of a private API. This constant may only be used in the Homebrew/brew repository. Third parties should avoid using this constant if possible, as it may be removed or changed without warning.

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.

T.let(DateTime.new(2024, 3, 14).freeze, DateTime)

Class Method Summary collapse

Class Method Details

.check_attestation(bottle, signing_repo, signing_workflow = nil, subject = nil) ⇒ Hash{T.untyped => T.untyped}

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

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.

Parameters:

  • bottle (Bottle)
  • signing_repo (String)
  • signing_workflow (String, nil) (defaults to: nil)
  • subject (String, nil) (defaults to: nil)

Returns:

  • (Hash{T.untyped => T.untyped})

    the JSON-decoded response.

Raises:



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'attestation.rb', line 66

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

.check_core_attestation(bottle) ⇒ Hash{T.untyped => T.untyped}

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

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.

Parameters:

Returns:

  • (Hash{T.untyped => T.untyped})

    the JSON-decoded response

Raises:



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'attestation.rb', line 107

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}"

    # We don't pass in a signing workflow for backfill signatures because
    # some backfilled bottle signatures were signed from the 'backfill'
    # branch, and others from 'main' of trailofbits/homebrew-brew-verify
    # so the signing workflow is slightly different which causes some bottles to incorrectly
    # fail when checking their attestation. This shouldn't meaningfully affect security
    # because if somehow someone could generate false backfill attestations
    # from a different workflow we will still catch it because the
    # attestation would have been generated after our cutoff date.
    backfill_attestation = check_attestation bottle, BACKFILL_REPO, nil, 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

.gh_executablePathname

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Returns a path to a suitable gh executable for attestation verification.

Returns:



40
41
42
43
44
45
46
47
# File 'attestation.rb', line 40

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