| 
									
										
										
										
											2024-04-09 11:03:41 -04:00
										 |  |  | # typed: strict | 
					
						
							| 
									
										
										
										
											2024-04-08 16:18:15 -04:00
										 |  |  | # frozen_string_literal: true | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | require "date" | 
					
						
							|  |  |  | require "json" | 
					
						
							|  |  |  | require "utils/popen" | 
					
						
							| 
									
										
										
										
											2024-07-15 13:30:47 -04:00
										 |  |  | require "utils/github/api" | 
					
						
							| 
									
										
										
										
											2024-04-08 16:18:15 -04:00
										 |  |  | require "exceptions" | 
					
						
							| 
									
										
										
										
											2024-05-14 14:32:23 -04:00
										 |  |  | require "system_command" | 
					
						
							| 
									
										
										
										
											2025-08-20 19:20:19 +01:00
										 |  |  | require "utils/output" | 
					
						
							| 
									
										
										
										
											2024-04-08 16:18:15 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | module Homebrew | 
					
						
							|  |  |  |   module Attestation | 
					
						
							| 
									
										
										
										
											2024-05-14 14:32:23 -04:00
										 |  |  |     extend SystemCommand::Mixin | 
					
						
							| 
									
										
										
										
											2025-08-20 19:20:19 +01:00
										 |  |  |     extend Utils::Output::Mixin | 
					
						
							| 
									
										
										
										
											2024-05-14 14:32:23 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-08 16:22:57 -04:00
										 |  |  |     # @api private | 
					
						
							| 
									
										
										
										
											2024-04-08 16:18:15 -04:00
										 |  |  |     HOMEBREW_CORE_REPO = "Homebrew/homebrew-core" | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-08 16:22:57 -04:00
										 |  |  |     # @api private | 
					
						
							| 
									
										
										
										
											2024-04-08 16:18:15 -04:00
										 |  |  |     BACKFILL_REPO = "trailofbits/homebrew-brew-verify" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # No backfill attestations after this date are considered valid. | 
					
						
							| 
									
										
										
										
											2024-04-09 10:50:49 -04:00
										 |  |  |     # | 
					
						
							|  |  |  |     # 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. | 
					
						
							|  |  |  |     # | 
					
						
							| 
									
										
										
										
											2024-04-08 16:18:15 -04:00
										 |  |  |     # @api private | 
					
						
							| 
									
										
										
										
											2024-04-09 11:03:41 -04:00
										 |  |  |     BACKFILL_CUTOFF = T.let(DateTime.new(2024, 3, 14).freeze, DateTime) | 
					
						
							| 
									
										
										
										
											2024-04-08 16:18:15 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-23 16:59:52 +01:00
										 |  |  |     # Raised when the attestation was not found. | 
					
						
							|  |  |  |     # | 
					
						
							|  |  |  |     # @api private | 
					
						
							|  |  |  |     class MissingAttestationError < RuntimeError; end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-09 10:52:48 -04:00
										 |  |  |     # Raised when attestation verification fails. | 
					
						
							|  |  |  |     # | 
					
						
							|  |  |  |     # @api private | 
					
						
							|  |  |  |     class InvalidAttestationError < RuntimeError; end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-03 12:37:01 -04:00
										 |  |  |     # Raised if attestation verification cannot continue due to missing | 
					
						
							|  |  |  |     # credentials. | 
					
						
							|  |  |  |     # | 
					
						
							|  |  |  |     # @api private | 
					
						
							|  |  |  |     class GhAuthNeeded < RuntimeError; end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-18 16:11:25 +01:00
										 |  |  |     # Raised if attestation verification cannot continue due to invalid | 
					
						
							|  |  |  |     # credentials. | 
					
						
							|  |  |  |     # | 
					
						
							|  |  |  |     # @api private | 
					
						
							|  |  |  |     class GhAuthInvalid < RuntimeError; end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-10 12:06:09 +01:00
										 |  |  |     # Raised if attestation verification cannot continue due to `gh` | 
					
						
							|  |  |  |     # being incompatible with attestations, typically because it's too old. | 
					
						
							|  |  |  |     # | 
					
						
							|  |  |  |     # @api private | 
					
						
							|  |  |  |     class GhIncompatible < RuntimeError; end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-13 10:51:49 -04:00
										 |  |  |     # Returns whether attestation verification is enabled. | 
					
						
							|  |  |  |     # | 
					
						
							|  |  |  |     # @api private | 
					
						
							|  |  |  |     sig { returns(T::Boolean) } | 
					
						
							|  |  |  |     def self.enabled? | 
					
						
							| 
									
										
										
										
											2024-07-14 11:50:57 -04:00
										 |  |  |       return false if Homebrew::EnvConfig.no_verify_attestations? | 
					
						
							| 
									
										
										
										
											2024-07-13 14:58:07 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-10 09:10:08 +01:00
										 |  |  |       Homebrew::EnvConfig.verify_attestations? | 
					
						
							| 
									
										
										
										
											2024-07-13 10:51:49 -04:00
										 |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-09 10:48:17 -04:00
										 |  |  |     # Returns a path to a suitable `gh` executable for attestation verification. | 
					
						
							|  |  |  |     # | 
					
						
							|  |  |  |     # @api private | 
					
						
							| 
									
										
										
										
											2024-04-09 11:03:41 -04:00
										 |  |  |     sig { returns(Pathname) } | 
					
						
							| 
									
										
										
										
											2024-04-09 10:45:44 -04:00
										 |  |  |     def self.gh_executable | 
					
						
							| 
									
										
										
										
											2024-07-15 11:44:53 -04:00
										 |  |  |       @gh_executable ||= T.let(nil, T.nilable(Pathname)) | 
					
						
							| 
									
										
										
										
											2024-07-15 11:39:22 -04:00
										 |  |  |       return @gh_executable if @gh_executable.present? | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-29 12:59:29 -04:00
										 |  |  |       # 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. | 
					
						
							| 
									
										
										
										
											2024-07-15 11:39:22 -04:00
										 |  |  |       with_env(HOMEBREW_NO_VERIFY_ATTESTATIONS: "1") do | 
					
						
							| 
									
										
										
										
											2024-08-12 15:53:17 -04:00
										 |  |  |         @gh_executable = ensure_executable!("gh", reason: "verifying attestations", latest: true) | 
					
						
							| 
									
										
										
										
											2024-07-15 11:39:22 -04:00
										 |  |  |       end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-15 11:44:53 -04:00
										 |  |  |       T.must(@gh_executable) | 
					
						
							| 
									
										
										
										
											2024-04-09 10:45:44 -04:00
										 |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-18 21:32:50 -07:00
										 |  |  |     # Prioritize installing `gh` first if it's in the formula list | 
					
						
							|  |  |  |     # or check for the existence of the `gh` executable elsewhere. | 
					
						
							|  |  |  |     # | 
					
						
							|  |  |  |     # This ensures that a valid version of `gh` is installed before | 
					
						
							|  |  |  |     # we use it to check the attestations of any other formulae we | 
					
						
							|  |  |  |     # want to install. | 
					
						
							|  |  |  |     # | 
					
						
							|  |  |  |     # @api private | 
					
						
							|  |  |  |     sig { params(formulae: T::Array[Formula]).returns(T::Array[Formula]) } | 
					
						
							|  |  |  |     def self.sort_formulae_for_install(formulae) | 
					
						
							|  |  |  |       if formulae.include?(Formula["gh"]) | 
					
						
							|  |  |  |         [Formula["gh"]] | formulae | 
					
						
							|  |  |  |       else | 
					
						
							|  |  |  |         Homebrew::Attestation.gh_executable | 
					
						
							|  |  |  |         formulae | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-08 16:18:15 -04:00
										 |  |  |     # Verifies the given bottle against a cryptographic attestation of build provenance. | 
					
						
							|  |  |  |     # | 
					
						
							| 
									
										
										
										
											2024-06-10 09:31:53 +01:00
										 |  |  |     # The provenance is verified as originating from `signing_repository`, which is a `String` | 
					
						
							|  |  |  |     # that should be formatted as a GitHub `owner/repository`. | 
					
						
							| 
									
										
										
										
											2024-04-08 16:18:15 -04:00
										 |  |  |     # | 
					
						
							|  |  |  |     # 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. | 
					
						
							| 
									
										
										
										
											2024-05-03 12:37:01 -04:00
										 |  |  |     # @raise [GhAuthNeeded] on any authentication failures | 
					
						
							| 
									
										
										
										
											2024-04-08 16:22:57 -04:00
										 |  |  |     # @raise [InvalidAttestationError] on any verification failures | 
					
						
							| 
									
										
										
										
											2024-04-08 16:18:15 -04:00
										 |  |  |     # | 
					
						
							|  |  |  |     # @api private | 
					
						
							| 
									
										
										
										
											2024-04-09 11:03:41 -04:00
										 |  |  |     sig { | 
					
						
							|  |  |  |       params(bottle: Bottle, signing_repo: String, | 
					
						
							| 
									
										
										
										
											2024-04-11 16:44:57 -04:00
										 |  |  |              signing_workflow: T.nilable(String), subject: T.nilable(String)).returns(T::Hash[T.untyped, T.untyped]) | 
					
						
							| 
									
										
										
										
											2024-04-09 11:03:41 -04:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2024-04-11 16:44:57 -04:00
										 |  |  |     def self.check_attestation(bottle, signing_repo, signing_workflow = nil, subject = nil) | 
					
						
							| 
									
										
										
										
											2024-05-14 14:32:23 -04:00
										 |  |  |       cmd = ["attestation", "verify", bottle.cached_download, "--repo", signing_repo, "--format", | 
					
						
							| 
									
										
										
										
											2024-04-09 10:45:44 -04:00
										 |  |  |              "json"] | 
					
						
							| 
									
										
										
										
											2024-04-08 16:18:15 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-09 10:18:08 -04:00
										 |  |  |       cmd += ["--cert-identity", signing_workflow] if signing_workflow.present? | 
					
						
							| 
									
										
										
										
											2024-04-08 16:18:15 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-03 12:37:01 -04:00
										 |  |  |       # Fail early if we have no credentials. The command below invariably | 
					
						
							| 
									
										
										
										
											2024-07-18 12:06:37 -04:00
										 |  |  |       # fails without them, so this saves us an unnecessary subshell. | 
					
						
							| 
									
										
										
										
											2024-05-03 12:37:01 -04:00
										 |  |  |       credentials = GitHub::API.credentials | 
					
						
							|  |  |  |       raise GhAuthNeeded, "missing credentials" if credentials.blank? | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-08 16:18:15 -04:00
										 |  |  |       begin | 
					
						
							| 
									
										
										
										
											2024-07-18 16:11:25 +01:00
										 |  |  |         result = system_command!(gh_executable, args: cmd, | 
					
						
							|  |  |  |                                  env: { "GH_TOKEN" => credentials, "GH_HOST" => "github.com" }, | 
					
						
							| 
									
										
										
										
											2024-07-17 17:26:59 +01:00
										 |  |  |                                  secrets: [credentials], print_stderr: false, chdir: HOMEBREW_TEMP) | 
					
						
							| 
									
										
										
										
											2024-04-08 16:18:15 -04:00
										 |  |  |       rescue ErrorDuringExecution => e | 
					
						
							| 
									
										
										
										
											2024-10-10 12:06:09 +01:00
										 |  |  |         if e.status.exitstatus == 1 && e.stderr.include?("unknown command") | 
					
						
							|  |  |  |           raise GhIncompatible, "gh CLI is incompatible with attestations" | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-03 12:37:01 -04:00
										 |  |  |         # Even if we have credentials, they may be invalid or malformed. | 
					
						
							| 
									
										
										
										
											2024-07-18 16:11:25 +01:00
										 |  |  |         if e.status.exitstatus == 4 || e.stderr.include?("HTTP 401: Bad credentials") | 
					
						
							|  |  |  |           raise GhAuthInvalid, "invalid credentials" | 
					
						
							|  |  |  |         end | 
					
						
							| 
									
										
										
										
											2024-05-03 12:37:01 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-23 16:59:52 +01:00
										 |  |  |         raise MissingAttestationError, "attestation not found: #{e}" if e.stderr.include?("HTTP 404: Not Found") | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-08 16:18:15 -04:00
										 |  |  |         raise InvalidAttestationError, "attestation verification failed: #{e}" | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       begin | 
					
						
							| 
									
										
										
										
											2024-05-14 14:32:23 -04:00
										 |  |  |         attestations = JSON.parse(result.stdout) | 
					
						
							| 
									
										
										
										
											2024-04-08 16:21:31 -04:00
										 |  |  |       rescue JSON::ParserError | 
					
						
							| 
									
										
										
										
											2024-04-08 16:18:15 -04:00
										 |  |  |         raise InvalidAttestationError, "attestation verification returned malformed JSON" | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-11 13:39:13 -04:00
										 |  |  |       # `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 | 
					
						
							| 
									
										
										
										
											2024-12-05 14:11:00 -05:00
										 |  |  |       # filter these down to just the attestation whose subject(s) contain the bottle's name. | 
					
						
							|  |  |  |       # As of 2024-12-04 GitHub's Artifact Attestation feature can put multiple subjects | 
					
						
							|  |  |  |       # in a single attestation, so we check every subject in each attestation | 
					
						
							|  |  |  |       # and select the first attestation with a matching subject. | 
					
						
							| 
									
										
										
										
											2024-12-05 14:16:30 -05:00
										 |  |  |       # In particular, this happens with v2.0.0 and later of the | 
					
						
							|  |  |  |       # `actions/attest-build-provenance` action. | 
					
						
							| 
									
										
										
										
											2024-04-11 16:44:57 -04:00
										 |  |  |       subject = bottle.filename.to_s if subject.blank? | 
					
						
							| 
									
										
										
										
											2024-06-06 11:23:03 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  |       attestation = if bottle.tag.to_sym == :all | 
					
						
							|  |  |  |         # :all-tagged bottles are created by `brew bottle --merge`, and are not directly | 
					
						
							|  |  |  |         # bound to their own filename (since they're created by deduplicating other filenames). | 
					
						
							|  |  |  |         # To verify these, we parse each attestation subject and look for one with a matching | 
					
						
							|  |  |  |         # formula (name, version), but not an exact tag match. | 
					
						
							|  |  |  |         # This is sound insofar as the signature has already been verified. However, | 
					
						
							|  |  |  |         # longer term, we should also directly attest to `:all`-tagged bottles. | 
					
						
							|  |  |  |         attestations.find do |a| | 
					
						
							| 
									
										
										
										
											2024-12-05 14:11:00 -05:00
										 |  |  |           candidate_subjects = a.dig("verificationResult", "statement", "subject") | 
					
						
							|  |  |  |           candidate_subjects.any? do |candidate| | 
					
						
							|  |  |  |             candidate["name"].start_with? "#{bottle.filename.name}--#{bottle.filename.version}" | 
					
						
							|  |  |  |           end | 
					
						
							| 
									
										
										
										
											2024-06-06 11:23:03 -04:00
										 |  |  |         end | 
					
						
							|  |  |  |       else | 
					
						
							|  |  |  |         attestations.find do |a| | 
					
						
							| 
									
										
										
										
											2024-12-05 14:11:00 -05:00
										 |  |  |           candidate_subjects = a.dig("verificationResult", "statement", "subject") | 
					
						
							|  |  |  |           candidate_subjects.any? { |candidate| candidate["name"] == subject } | 
					
						
							| 
									
										
										
										
											2024-06-06 11:23:03 -04:00
										 |  |  |         end | 
					
						
							| 
									
										
										
										
											2024-04-11 13:39:13 -04:00
										 |  |  |       end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-26 14:26:45 -04:00
										 |  |  |       raise InvalidAttestationError, "no attestation matches subject: #{subject}" if attestation.blank? | 
					
						
							| 
									
										
										
										
											2024-04-08 16:18:15 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-11 13:39:13 -04:00
										 |  |  |       attestation | 
					
						
							| 
									
										
										
										
											2024-04-08 16:18:15 -04:00
										 |  |  |     end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-04 12:39:58 +08:00
										 |  |  |     ATTESTATION_MAX_RETRIES = 5
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-08 16:18:15 -04:00
										 |  |  |     # 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. | 
					
						
							| 
									
										
										
										
											2024-04-08 16:22:57 -04:00
										 |  |  |     # | 
					
						
							|  |  |  |     # @return [Hash] the JSON-decoded response | 
					
						
							| 
									
										
										
										
											2024-05-03 12:37:01 -04:00
										 |  |  |     # @raise [GhAuthNeeded] on any authentication failures | 
					
						
							| 
									
										
										
										
											2024-04-08 16:22:57 -04:00
										 |  |  |     # @raise [InvalidAttestationError] on any verification failures | 
					
						
							|  |  |  |     # | 
					
						
							|  |  |  |     # @api private | 
					
						
							| 
									
										
										
										
											2024-04-09 11:03:41 -04:00
										 |  |  |     sig { params(bottle: Bottle).returns(T::Hash[T.untyped, T.untyped]) } | 
					
						
							| 
									
										
										
										
											2024-04-08 16:18:15 -04:00
										 |  |  |     def self.check_core_attestation(bottle) | 
					
						
							|  |  |  |       begin | 
					
						
							| 
									
										
										
										
											2024-05-18 10:04:53 -04:00
										 |  |  |         # Ideally, we would also constrain the signing workflow here, but homebrew-core | 
					
						
							|  |  |  |         # currently uses multiple signing workflows to produce bottles | 
					
						
							|  |  |  |         # (e.g. `dispatch-build-bottle.yml`, `dispatch-rebottle.yml`, etc.). | 
					
						
							|  |  |  |         # | 
					
						
							|  |  |  |         # We could check each of these (1) explicitly (slow), (2) by generating a pattern | 
					
						
							|  |  |  |         # to pass into `--cert-identity-regex` (requires us to build up a Go-style regex), | 
					
						
							|  |  |  |         # or (3) by checking the resulting JSON for the expected signing workflow. | 
					
						
							|  |  |  |         # | 
					
						
							|  |  |  |         # Long term, we should probably either do (3) *or* switch to a single reusable | 
					
						
							|  |  |  |         # workflow, which would then be our sole identity. However, GitHub's | 
					
						
							|  |  |  |         # attestations currently do not include reusable workflow state by default. | 
					
						
							|  |  |  |         attestation = check_attestation bottle, HOMEBREW_CORE_REPO | 
					
						
							| 
									
										
										
										
											2024-04-08 16:18:15 -04:00
										 |  |  |         return attestation | 
					
						
							| 
									
										
										
										
											2024-07-23 16:59:52 +01:00
										 |  |  |       rescue MissingAttestationError | 
					
						
							| 
									
										
										
										
											2025-07-25 15:44:22 +00:00
										 |  |  |         odebug "falling back on backfilled attestation for #{bottle.filename}" | 
					
						
							| 
									
										
										
										
											2024-04-11 16:44:57 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  |         # 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). | 
					
						
							| 
									
										
										
										
											2024-07-26 14:26:45 -04:00
										 |  |  |         url_sha256 = if EnvConfig.bottle_domain == HOMEBREW_BOTTLE_DEFAULT_DOMAIN | 
					
						
							|  |  |  |           Digest::SHA256.hexdigest(bottle.url) | 
					
						
							|  |  |  |         else | 
					
						
							|  |  |  |           # If our bottle is coming from a mirror, we need to recompute the expected | 
					
						
							|  |  |  |           # non-mirror URL to make the hash match. | 
					
						
							|  |  |  |           path, = Utils::Bottles.path_resolved_basename HOMEBREW_BOTTLE_DEFAULT_DOMAIN, bottle.name, | 
					
						
							|  |  |  |                                                         bottle.resource.checksum, bottle.filename | 
					
						
							|  |  |  |           url = "#{HOMEBREW_BOTTLE_DEFAULT_DOMAIN}/#{path}" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           Digest::SHA256.hexdigest(url) | 
					
						
							|  |  |  |         end | 
					
						
							| 
									
										
										
										
											2024-04-11 16:44:57 -04:00
										 |  |  |         subject = "#{url_sha256}--#{bottle.filename}" | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-30 10:50:33 -04:00
										 |  |  |         # We don't pass in a signing workflow for backfill signatures because | 
					
						
							|  |  |  |         # some backfilled bottle signatures were signed from the 'backfill' | 
					
						
							| 
									
										
										
										
											2024-04-30 10:52:36 -04:00
										 |  |  |         # 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 | 
					
						
							| 
									
										
										
										
											2024-04-30 10:50:33 -04:00
										 |  |  |         # 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. | 
					
						
							| 
									
										
										
										
											2024-04-30 09:52:04 -04:00
										 |  |  |         backfill_attestation = check_attestation bottle, BACKFILL_REPO, nil, subject | 
					
						
							| 
									
										
										
										
											2024-04-11 13:39:13 -04:00
										 |  |  |         timestamp = backfill_attestation.dig("verificationResult", "verifiedTimestamps", | 
					
						
							| 
									
										
										
										
											2024-04-08 16:18:15 -04:00
										 |  |  |                                              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 | 
					
						
							| 
									
										
										
										
											2024-10-04 12:39:58 +08:00
										 |  |  |     rescue InvalidAttestationError | 
					
						
							|  |  |  |       @attestation_retry_count ||= T.let(Hash.new(0), T.nilable(T::Hash[Bottle, Integer])) | 
					
						
							|  |  |  |       raise if @attestation_retry_count[bottle] >= ATTESTATION_MAX_RETRIES | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       sleep_time = 3 ** @attestation_retry_count[bottle] | 
					
						
							| 
									
										
										
										
											2024-10-09 23:24:24 +08:00
										 |  |  |       opoo "Failed to verify attestation. Retrying in #{sleep_time}s..." | 
					
						
							| 
									
										
										
										
											2024-10-04 16:00:29 +08:00
										 |  |  |       sleep sleep_time if ENV["HOMEBREW_TESTS"].blank? | 
					
						
							| 
									
										
										
										
											2024-10-04 12:39:58 +08:00
										 |  |  |       @attestation_retry_count[bottle] += 1
 | 
					
						
							|  |  |  |       retry | 
					
						
							| 
									
										
										
										
											2024-04-08 16:18:15 -04:00
										 |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | end |