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

Activate GPG verification upon install #8749

Closed
wants to merge 11 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
1 change: 1 addition & 0 deletions lib/hbc.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class Hbc; end
require 'hbc/dsl'
require 'hbc/exceptions'
require 'hbc/fetcher'
require 'hbc/gpg_check'
require 'hbc/hardware'
require 'hbc/installer'
require 'hbc/locations'
Expand Down
4 changes: 2 additions & 2 deletions lib/hbc/dsl/gpg.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ class Hbc::DSL::Gpg

def initialize(signature, parameters={})
@parameters = parameters
@signature = Hbc::UnderscoreSupportingURI.parse(signature)
@signature = Hbc::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 = Hbc::UnderscoreSupportingURI.parse(hvalue) if hkey == :key_url
hvalue = Hbc::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 lib/hbc/exceptions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,20 @@ def to_s
EOS
end
end

class Hbc::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
44 changes: 28 additions & 16 deletions lib/hbc/gpg_check.rb
Original file line number Diff line number Diff line change
@@ -1,29 +1,38 @@
class Hbc::GpgCheck
attr_reader :available
attr_reader :cask, :available, :signature, :successful

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

@available = @cask.gpg ? installed? : false
@signature = retrieve_signature(force_fetch)
@successful = nil
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 retrieve_signature(force=false)
maybe_dir = @cask.metadata_subdir('gpg')
versioned_cask = @cask.version.is_a?(String)

def fetch_sig(force=false)
unversioned_cask = @cask.version.is_a?(Symbol)
cached = @cask.metadata_subdir('gpg') unless unversioned_cask
# 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")
sig_path = meta_dir.join('signature.asc')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this assume the signature is always in the signature.asc file? This may not always be the case—and actually isn't for a lot of software. The creators may have a signature with a different name, or (as is most common) may have clearsigned the sha256 hashes of all the executables they serve for their project.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, that is merely the path to our local copy of the signature.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, sorry for not researching better what that was :)


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

sig_path
end
Expand All @@ -34,17 +43,20 @@ def import_key
when @cask.gpg.key_url then ['--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(file)
import_key
sig = fetch_sig

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

@command.run!('gpg',
:args => ['--verify', sig, file],
:print_stdout => true)
@successful = check.success?
end
end
2 changes: 2 additions & 0 deletions lib/hbc/installer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ def install(force=false, skip_cask_deps=false)
install_artifacts
save_caskfile force
enable_accessibility_access
rescue Hbc::CaskGpgVerificationFailedError => e
raise e
rescue StandardError => e
purge_versioned_files
raise e
Expand Down
11 changes: 11 additions & 0 deletions lib/hbc/verify.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
module Hbc::Verify
def self.all(path, cask)
checksum(path, cask)
gpg_signature(path, cask)
end

def self.checksum(path, cask)
Expand All @@ -22,4 +23,14 @@ def self.checksum(path, cask)
raise Hbc::CaskSha256MismatchError.new(path, expected, computed)
end
end

def self.gpg_signature(path, cask)
return unless cask.gpg
gpg = Hbc::GpgCheck.new(cask)

if gpg.available
gpg.verify(path)
raise Hbc::CaskGpgVerificationFailedError.new(cask.token, path, gpg.signature) unless gpg.successful
end
end
end
19 changes: 19 additions & 0 deletions test/cask/installer_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,25 @@
no_checksum.must_be :installed?
end

it "blows up on a bad gpg signature, if gpg is available" do
tampered = Hbc.load('bad-gpg-signature')
if Hbc::GpgCheck.new(tampered).available
lambda {
shutup do
Hbc::Installer.new(tampered).install
end
}.must_raise(Hbc::CaskGpgVerificationFailedError)
end
end

it "works fine with a good gpg signature" do
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 test/support/Casks/bad-gpg-signature.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
cask :v1test => '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_id => 'd8692123c4065dea5e0f3ab5249b39d24f25e3b6'

stage_only true
end
11 changes: 11 additions & 0 deletions test/support/Casks/good-gpg-signature.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
cask :v1test => '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_id => 'd8692123c4065dea5e0f3ab5249b39d24f25e3b6'

stage_only true
end
Binary file added test/support/binaries/gnupg-2.1.1.tar.bz2
Binary file not shown.
Binary file not shown.
Binary file added test/support/gnupg-2.1.1.tar.bz2.sig
Binary file not shown.
8 changes: 8 additions & 0 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,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
Hbc.load('basic-cask')
end
Expand Down