Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cask: activate GPG verification on install #1335

Closed
wants to merge 16 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Library/Homebrew/cask/lib/hbc/dsl/gpg.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ class Gpg

def initialize(signature, parameters = {})
@parameters = parameters
@signature = UnderscoreSupportingURI.parse(signature)
@signature = URL.new(signature)
parameters.each do |hkey, hvalue|
raise "invalid 'gpg' parameter: '#{hkey.inspect}'" unless VALID_PARAMETERS.include?(hkey)
writer_method = "#{hkey}=".to_sym
hvalue = UnderscoreSupportingURI.parse(hvalue) if hkey == :key_url
hvalue = URL.new(hvalue) if hkey == :key_url
valid_id?(hvalue) if hkey == :key_id
send(writer_method, hvalue)
end
Expand Down
17 changes: 17 additions & 0 deletions Library/Homebrew/cask/lib/hbc/exceptions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -160,4 +160,21 @@ def to_s
EOS
end
end

class CaskGpgVerificationFailedError < RuntimeError
attr_reader :path, :token, :signature
def initialize(token, path, signature)
@token = token
@path = path
@signature = signature
end

def to_s
<<-EOS.undent
GPG failed to verify the authenticity of #{token}.
Signature: #{signature}
File: #{path}
EOS
end
end
end
2 changes: 2 additions & 0 deletions Library/Homebrew/cask/lib/hbc/installer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ def install
install_artifacts
save_caskfile
enable_accessibility_access
rescue CaskGpgVerificationFailedError => e
raise e
rescue StandardError => e
purge_versioned_files
raise e
Expand Down
4 changes: 2 additions & 2 deletions Library/Homebrew/cask/lib/hbc/verify.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ module Verify

def verifications
[
Hbc::Verify::Checksum
# TODO: Hbc::Verify::Gpg
Hbc::Verify::Checksum,
Hbc::Verify::Gpg
]
end

Expand Down
53 changes: 37 additions & 16 deletions Library/Homebrew/cask/lib/hbc/verify/gpg.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ def self.me?(cask)
cask.gpg
end

attr_reader :cask, :downloaded_path
attr_reader :cask, :downloaded_path, :force_fetch

def initialize(cask, downloaded_path, command = Hbc::SystemCommand)
def initialize(cask, downloaded_path, force_fetch = false, command = Hbc::SystemCommand)
@command = command
@cask = cask
@downloaded_path = downloaded_path
@force_fetch = force_fetch
end

def available?
Expand All @@ -19,21 +20,28 @@ def available?
end

def installed?
cmd = @command.run("/usr/bin/type",
args: ["-p", "gpg"])
gpg_bin_path = @command.run("/usr/bin/type",
args: ["-p", "gpg"])

# if `gpg` is found, return its absolute path
cmd.success? ? cmd.stdout : false
gpg_bin_path.success? ? gpg_bin_path.stdout : false
end

def fetch_sig(force = false)
unversioned_cask = cask.version.is_a?(Symbol)
cached = cask.metadata_subdir("gpg") unless unversioned_cask
def retrieve_signature
maybe_dir = cask.metadata_subdir("gpg")
versioned_cask = cask.version.is_a?(String)

# maybe_dir may be:
# - nil, in the absence of a parent metadata directory;
# - the path to a non-existent /gpg subdir of the metadata directory,
# if the most recent metadata directory was not created by GpgCheck;
# - the path to an existing /gpg subdir, where a signature was previously
# saved.
cached = maybe_dir if versioned_cask && maybe_dir && maybe_dir.exist?

meta_dir = cached || cask.metadata_subdir("gpg", :now, true)
sig_path = meta_dir.join("signature.asc")

curl(cask.gpg.signature, "-o", sig_path.to_s) unless cached || force
curl(cask.gpg.signature, '-o', sig_path) if !cached || !sig_path.exist? || force_fetch

sig_path
end
Expand All @@ -45,19 +53,32 @@ def import_key
["--fetch-key", cask.gpg.key_url.to_s]
end

@command.run!("gpg", args: args)
import = @command.run("gpg", args: args,
print_stderr: true)
unless import.success?
raise CaskError.new("GPG failed to retrieve the #{cask} signing key: #{cask.gpg.key_id || cask.gpg.key_url}")
end
end

def verify
return unless available?
unless available?
opoo <<-EOS.undent
Skipping GPG signature for #{cask} because gpg is not available.
To enable GPG signature verification, install gpg with:

brew install gpg
EOS
return
end

import_key
sig = fetch_sig
signature = retrieve_signature

ohai "Verifying GPG signature for #{cask}"
check = @command.run("gpg", args: ["--verify", signature, downloaded_path],
print_stdout: true)

@command.run!("gpg",
args: ["--verify", sig, downloaded_path],
print_stdout: true)
raise CaskGpgVerificationFailedError.new(cask.token, downloaded_path, signature) unless check.success?
end
end
end
Expand Down
19 changes: 19 additions & 0 deletions Library/Homebrew/cask/test/cask/installer_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,25 @@
no_checksum.must_be :installed?
end

it "blows up on a bad gpg signature" do
skip("gpg not installed") if which("gpg").nil?
tampered = Hbc.load('bad-gpg-signature')
lambda {
shutup do
Hbc::Installer.new(tampered).install
end
}.must_raise(Hbc::CaskGpgVerificationFailedError)
end

it "works fine with a good gpg signature" do
skip("gpg not installed") if which("gpg").nil?
signed = Hbc.load('good-gpg-signature')
shutup do
Hbc::Installer.new(signed).install
end
signed.must_be :installed?
end

it "prints caveats if they're present" do
with_caveats = Hbc.load("with-caveats")
TestHelper.must_output(self, lambda {
Expand Down
11 changes: 11 additions & 0 deletions Library/Homebrew/cask/test/support/Casks/bad-gpg-signature.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
test_cask 'bad-gpg-signature' do
version '2.1.1_tampered'
sha256 '2be45cd67fc081b9e9cdbcc5b43a74a010ae2ae0a8ee9efdf7295e7a1e144143'

url TestHelper.local_binary_url('gnupg-2.1.1_tampered.tar.bz2')
homepage 'http://example.com/bad-gpg-ignature'
gpg TestHelper.local_support_url('gnupg-2.1.1.tar.bz2.sig'),
key_url: 'https://www.gnupg.org/signature_key.html'

stage_only true
end
11 changes: 11 additions & 0 deletions Library/Homebrew/cask/test/support/Casks/good-gpg-signature.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
test_cask 'good-gpg-signature' do
version '2.1.1'
sha256 '70ecd01d2875db62624c911c2fd815742f50aef5492698eb3bfc09a08690ce49'

url TestHelper.local_binary_url('gnupg-2.1.1.tar.bz2')
homepage 'http://example.com/good-gpg-signature'
gpg TestHelper.local_support_url('gnupg-2.1.1.tar.bz2.sig'),
key_url: 'https://www.gnupg.org/signature_key.html'

stage_only true
end
Binary file not shown.
Binary file not shown.
Binary file not shown.
8 changes: 8 additions & 0 deletions Library/Homebrew/cask/test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,18 @@ def self.local_binary_path(name)
File.expand_path(File.join(File.dirname(__FILE__), "support", "binaries", name))
end

def self.local_support_path(name)
File.expand_path(File.join(File.dirname(__FILE__), 'support', name))
end

def self.local_binary_url(name)
"file://" + local_binary_path(name)
end

def self.local_support_url(name)
'file://' + local_support_path(name)
end

def self.test_cask
@test_cask ||= Hbc.load("basic-cask")
end
Expand Down