attestation: add initial attestation helpers
Adds the basic attestation verification APIs, as well as a pre-pour check against `HOMEBREW_VERIFY_ATTESTATIONS` that verifies the attestation (or backfill as necessary) for bottles from homebrew-core. Signed-off-by: William Woodruff <william@yossarian.net>
This commit is contained in:
parent
a673589389
commit
48e39bb51d
82
Library/Homebrew/attestation.rb
Normal file
82
Library/Homebrew/attestation.rb
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
# typed: true
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "date"
|
||||||
|
require "json"
|
||||||
|
require "utils/popen"
|
||||||
|
require "exceptions"
|
||||||
|
|
||||||
|
module Homebrew
|
||||||
|
module Attestation
|
||||||
|
HOMEBREW_CORE_REPO = "Homebrew/homebrew-core"
|
||||||
|
HOMEBREW_CORE_CI_URI = "https://github.com/Homebrew/homebrew-core/.github/workflows/publish-commit-bottles.yml@refs/heads/master"
|
||||||
|
|
||||||
|
BACKFILL_REPO = "trailofbits/homebrew-brew-verify"
|
||||||
|
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.
|
||||||
|
# @api private
|
||||||
|
BACKFILL_CUTOFF = DateTime.new(2024, 3, 14)
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
# @raises [InvalidAttestationError] on any verification failures.
|
||||||
|
#
|
||||||
|
# @api private
|
||||||
|
def self.check_attestation(bottle, signing_repo, signing_workflow = nil)
|
||||||
|
cmd = [HOMEBREW_GH, "attestation", "verify", bottle.cached_download, "--repo", signing_repo, "--format", "json"]
|
||||||
|
|
||||||
|
unless signing_workflow.nil?
|
||||||
|
cmd += ["--cert-identity", signing_workflow]
|
||||||
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
output = Utils.safe_popen_read(*cmd)
|
||||||
|
rescue ErrorDuringExecution => e
|
||||||
|
raise InvalidAttestationError, "attestation verification failed: #{e}"
|
||||||
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
data = JSON.parse(output)
|
||||||
|
rescue JSON::ParserError => e
|
||||||
|
raise InvalidAttestationError, "attestation verification returned malformed JSON"
|
||||||
|
end
|
||||||
|
|
||||||
|
raise InvalidAttestationError, "attestation output is empty" if data.empty?
|
||||||
|
|
||||||
|
data
|
||||||
|
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.
|
||||||
|
def self.check_core_attestation(bottle)
|
||||||
|
begin
|
||||||
|
attestation = check_attestation bottle, HOMEBREW_CORE_REPO
|
||||||
|
return attestation
|
||||||
|
rescue InvalidAttestationError
|
||||||
|
odebug "falling back on backfilled attestation"
|
||||||
|
backfill_attestation = check_attestation bottle, BACKFILL_REPO, BACKFILL_REPO_CI_URI
|
||||||
|
timestamp = backfill_attestation.dig(0, "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
|
||||||
@ -454,8 +454,23 @@ setup_git() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setup_gh() {
|
||||||
|
# This is set by the user environment.
|
||||||
|
# shellcheck disable=SC2154
|
||||||
|
if [[ -n "${HOMEBREW_VERIFY_ATTESTATIONS}" && -x "${HOMEBREW_PREFIX}/opt/gh/bin/gh" ]]
|
||||||
|
then
|
||||||
|
HOMEBREW_GH="${HOMEBREW_PREFIX}/opt/gh/bin/gh"
|
||||||
|
elif [[ -n "${HOMEBREW_GH_PATH}" ]]
|
||||||
|
then
|
||||||
|
HOMEBREW_GH="${HOMEBREW_GH_PATH}"
|
||||||
|
else
|
||||||
|
HOMEBREW_GH="gh"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
setup_curl
|
setup_curl
|
||||||
setup_git
|
setup_git
|
||||||
|
setup_gh
|
||||||
|
|
||||||
HOMEBREW_VERSION="$("${HOMEBREW_GIT}" -C "${HOMEBREW_REPOSITORY}" describe --tags --dirty --abbrev=7 2>/dev/null)"
|
HOMEBREW_VERSION="$("${HOMEBREW_GIT}" -C "${HOMEBREW_REPOSITORY}" describe --tags --dirty --abbrev=7 2>/dev/null)"
|
||||||
HOMEBREW_USER_AGENT_VERSION="${HOMEBREW_VERSION}"
|
HOMEBREW_USER_AGENT_VERSION="${HOMEBREW_VERSION}"
|
||||||
@ -721,6 +736,7 @@ export HOMEBREW_API_DEFAULT_DOMAIN
|
|||||||
export HOMEBREW_BOTTLE_DEFAULT_DOMAIN
|
export HOMEBREW_BOTTLE_DEFAULT_DOMAIN
|
||||||
export HOMEBREW_CURL_SPEED_LIMIT
|
export HOMEBREW_CURL_SPEED_LIMIT
|
||||||
export HOMEBREW_CURL_SPEED_TIME
|
export HOMEBREW_CURL_SPEED_TIME
|
||||||
|
export HOMEBREW_GH
|
||||||
|
|
||||||
if [[ -n "${HOMEBREW_MACOS}" && -x "/usr/bin/xcode-select" ]]
|
if [[ -n "${HOMEBREW_MACOS}" && -x "/usr/bin/xcode-select" ]]
|
||||||
then
|
then
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
# HOMEBREW_LIBRARY, HOMEBREW_PREFIX, HOMEBREW_REPOSITORY are set by bin/brew
|
# HOMEBREW_LIBRARY, HOMEBREW_PREFIX, HOMEBREW_REPOSITORY are set by bin/brew
|
||||||
# HOMEBREW_BREW_DEFAULT_GIT_REMOTE, HOMEBREW_BREW_GIT_REMOTE, HOMEBREW_CACHE, HOMEBREW_CELLAR, HOMEBREW_CURL
|
# HOMEBREW_BREW_DEFAULT_GIT_REMOTE, HOMEBREW_BREW_GIT_REMOTE, HOMEBREW_CACHE, HOMEBREW_CELLAR, HOMEBREW_CURL
|
||||||
# HOMEBREW_DEV_CMD_RUN, HOMEBREW_FORCE_BREWED_CURL, HOMEBREW_FORCE_BREWED_GIT, HOMEBREW_SYSTEM_CURL_TOO_OLD
|
# HOMEBREW_DEV_CMD_RUN, HOMEBREW_FORCE_BREWED_CURL, HOMEBREW_FORCE_BREWED_GIT, HOMEBREW_SYSTEM_CURL_TOO_OLD
|
||||||
# HOMEBREW_USER_AGENT_CURL are set by brew.sh
|
# HOMEBREW_USER_AGENT_CURL, HOMEBREW_GH are set by brew.sh
|
||||||
# shellcheck disable=SC2154
|
# shellcheck disable=SC2154
|
||||||
source "${HOMEBREW_LIBRARY}/Homebrew/utils/lock.sh"
|
source "${HOMEBREW_LIBRARY}/Homebrew/utils/lock.sh"
|
||||||
|
|
||||||
@ -415,6 +415,17 @@ user account:
|
|||||||
EOS
|
EOS
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# we need `gh` if the user enables attestation verification
|
||||||
|
if [[ -n "${HOMEBREW_VERIFY_ATTESTATIONS}" && ! -x "${HOMEBREW_GH}" ]]
|
||||||
|
then
|
||||||
|
# we cannot install `gh` if homebrew/core is unavailable.
|
||||||
|
# we don't enable attestations on `gh` itself, to prevent a bootstrap cycle.
|
||||||
|
if [[ -z "${HOMEBREW_CORE_AVAILABLE}" ]] || ! HOMEBREW_VERIFY_ATTESTATIONS='' brew install gh
|
||||||
|
then
|
||||||
|
odie "'gh' must be installed and in your PATH!"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# we may want to use Homebrew CA certificates
|
# we may want to use Homebrew CA certificates
|
||||||
if [[ -n "${HOMEBREW_FORCE_BREWED_CA_CERTIFICATES}" && ! -f "${HOMEBREW_PREFIX}/etc/ca-certificates/cert.pem" ]]
|
if [[ -n "${HOMEBREW_FORCE_BREWED_CA_CERTIFICATES}" && ! -f "${HOMEBREW_PREFIX}/etc/ca-certificates/cert.pem" ]]
|
||||||
then
|
then
|
||||||
|
|||||||
@ -422,6 +422,11 @@ module Homebrew
|
|||||||
"useful to avoid long-running Homebrew commands being killed due to no output.",
|
"useful to avoid long-running Homebrew commands being killed due to no output.",
|
||||||
boolean: true,
|
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: {
|
SUDO_ASKPASS: {
|
||||||
description: "If set, pass the `-A` option when calling `sudo`(8).",
|
description: "If set, pass the `-A` option when calling `sudo`(8).",
|
||||||
},
|
},
|
||||||
|
|||||||
@ -783,3 +783,6 @@ class CyclicDependencyError < RuntimeError
|
|||||||
EOS
|
EOS
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Raised when attestation verification fails.
|
||||||
|
class InvalidAttestationError < RuntimeError; end
|
||||||
|
|||||||
@ -22,6 +22,7 @@ require "utils/spdx"
|
|||||||
require "deprecate_disable"
|
require "deprecate_disable"
|
||||||
require "unlink"
|
require "unlink"
|
||||||
require "service"
|
require "service"
|
||||||
|
require "attestation"
|
||||||
|
|
||||||
# Installer for a formula.
|
# Installer for a formula.
|
||||||
#
|
#
|
||||||
@ -1256,6 +1257,11 @@ on_request: installed_on_request?, options:)
|
|||||||
|
|
||||||
sig { void }
|
sig { void }
|
||||||
def pour
|
def pour
|
||||||
|
if Homebrew::EnvConfig.verify_attestations? && formula.tap&.core_tap?
|
||||||
|
ohai "Verifying attestation for #{formula.name}"
|
||||||
|
Homebrew::Attestation.check_core_attestation formula.bottle
|
||||||
|
end
|
||||||
|
|
||||||
HOMEBREW_CELLAR.cd do
|
HOMEBREW_CELLAR.cd do
|
||||||
downloader.stage
|
downloader.stage
|
||||||
end
|
end
|
||||||
|
|||||||
@ -66,6 +66,8 @@ HOMEBREW_PULL_OR_COMMIT_URL_REGEX =
|
|||||||
%r[https://github\.com/([\w-]+)/([\w-]+)?/(?:pull/(\d+)|commit/[0-9a-fA-F]{4,40})]
|
%r[https://github\.com/([\w-]+)/([\w-]+)?/(?:pull/(\d+)|commit/[0-9a-fA-F]{4,40})]
|
||||||
HOMEBREW_BOTTLES_EXTNAME_REGEX = /\.([a-z0-9_]+)\.bottle\.(?:(\d+)\.)?tar\.gz$/
|
HOMEBREW_BOTTLES_EXTNAME_REGEX = /\.([a-z0-9_]+)\.bottle\.(?:(\d+)\.)?tar\.gz$/
|
||||||
|
|
||||||
|
HOMEBREW_GH = Pathname(ENV.fetch("HOMEBREW_GH")).freeze
|
||||||
|
|
||||||
require "env_config"
|
require "env_config"
|
||||||
require "macos_version"
|
require "macos_version"
|
||||||
require "os"
|
require "os"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user