Class: Simp::RpmSigner

Inherits:
Object
  • Object
show all
Extended by:
CommandUtils
Defined in:
lib/simp/rpm_signer.rb

Overview

Class to sign RPMs. Uses ‘gpg’ and ‘rpm’ executables.

Constant Summary collapse

@@gpg_keys =
Hash.new

Class Method Summary collapse

Methods included from CommandUtils

which

Class Method Details

.clear_gpg_keys_cacheObject



323
324
325
# File 'lib/simp/rpm_signer.rb', line 323

def self.clear_gpg_keys_cache
  @@gpg_keys.clear
end

.kill_gpg_agent(gpg_keydir) ⇒ Object

Kill the GPG agent operating with the specified key dir, if rpm version 4.13.0 or later.

Beginning with version 4.13.0, rpm stands up a gpg-agent when a signing operation is requested.



22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# File 'lib/simp/rpm_signer.rb', line 22

def self.kill_gpg_agent(gpg_keydir)
  return if Gem::Version.new(Simp::RPM.version) < Gem::Version.new('4.13.0')

  %x(gpg-agent --homedir #{gpg_keydir} -q >& /dev/null)
  if $? && ($?.exitstatus == 0)
    # gpg-agent is running for specified keydir, so query it for its pid
    output = %x{echo 'GETINFO pid' | gpg-connect-agent --homedir=#{gpg_keydir}}
    if $? && ($?.exitstatus == 0)
      pid = output.lines.first[1..-1].strip.to_i
      begin
        Process.kill(0, pid)
        Process.kill(15, pid)
      rescue Errno::ESRCH
        # No longer running, so nothing to do!
      end
    end
  end
end

.load_key(gpg_keydir, verbose = false) ⇒ Object

Loads metadata for a GPG key found in gpg_keydir.

The GPG key is to be used to sign RPMs. If the required metadata cannot be retrieved from files found in the gpg_keydir, the user will be prompted for it.

Parameters:

  • gpg_keydir

    The full path of the directory where the key resides

  • verbose (defaults to: false)

    Whether to log debug information.

Raises:

  • If the ‘gpg’ executable cannot be found, the GPG key directory does not exist or GPG key metadata cannot be determined via ‘gpg’



53
54
55
56
57
58
59
60
61
62
63
64
65
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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/simp/rpm_signer.rb', line 53

def self.load_key(gpg_keydir, verbose = false)
  which('gpg') || raise("ERROR: Cannot sign RPMs without 'gpg'")
  File.directory?(gpg_keydir) || raise("ERROR: Could not find GPG keydir '#{gpg_keydir}'")

  gpg_key = File.basename(gpg_keydir)

  if @@gpg_keys[gpg_key]
    return @@gpg_keys[gpg_key]
  end

  gpg_name = nil
  gpg_password = nil
  begin
    File.read("#{gpg_keydir}/gengpgkey").each_line do |ln|
      name_line = ln.split(/^\s*Name-Email:/)
      if name_line.length > 1
        gpg_name = name_line.last.strip
      end

      passwd_line = ln.split(/^\s*Passphrase:/)
      if passwd_line.length > 1
        gpg_password = passwd_line.last.strip
      end
    end
  rescue Errno::ENOENT
  end

  if gpg_name.nil?
    puts "Warning: Could not find valid e-mail address for use with GPG."
    puts "Please enter e-mail address to use:"
    gpg_name = $stdin.gets.strip
  end

  if gpg_password.nil?
    if File.exist?(%(#{gpg_keydir}/password))
      gpg_password = File.read(%(#{gpg_keydir}/password)).chomp
    end

    if gpg_password.nil?
      puts "Warning: Could not find a password in '#{gpg_keydir}/password'!"
      puts "Please enter your GPG key password:"
      system 'stty -echo'
      gpg_password = $stdin.gets.strip
      system 'stty echo'
    end
  end

  gpg_key_size = nil
  gpg_key_id = nil
  cmd = "gpg --with-colons --homedir=#{gpg_keydir} --list-keys '<#{gpg_name}>' 2>&1"
  puts "Executing: #{cmd}" if verbose
  %x(#{cmd}).each_line do |line|
    # See https://github.com/CSNW/gnupg/blob/master/doc/DETAILS
    # Index  Content
    #   0    record type
    #   2    key length
    #   4    keyID
    fields = line.split(':')
    if fields[0] && (fields[0] == 'pub')
      gpg_key_size = fields[2].to_i
      gpg_key_id = fields[4]
      break
    end
  end

  if !gpg_key_size || !gpg_key_id
    raise("Error getting GPG key ID or Key size metadata for #{gpg_name}")
  end

  @@gpg_keys[gpg_key] = {
    :dir      => gpg_keydir,
    :name     => gpg_name,
    :key_id   => gpg_key_id,
    :key_size => gpg_key_size,
    :password => gpg_password
  }
end

.sign_rpm(rpm, gpg_keydir, options = {}) ⇒ Object

Signs the given RPM with the GPG key found in gpg_keydir

Parameters:

  • rpm

    Fully qualified path to an RPM to be signed.

  • gpg_keydir

    The full path of the directory where the key resides.

  • options (defaults to: {})

    Options Hash

Returns:

  • Whether package signing operation succeeded

Raises:

  • RuntimeError if ‘rpmsign’ executable cannot be found, the ‘gpg ’executable cannot be found, the GPG key directory does not exist or the GPG key metadata cannot be determined via ‘gpg’



148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
# File 'lib/simp/rpm_signer.rb', line 148

def self.sign_rpm(rpm, gpg_keydir, options={})
  # This may be a little confusing...Although we're using 'rpm --resign'
  # in lieu of 'rpmsign --addsign', they are equivalent and the presence
  # of 'rpmsign' is a legitimate check that the 'rpm --resign' capability
  # is available (i.e., rpm-sign package has been installed).
  which('rpmsign') || raise("ERROR: Cannot sign RPMs without 'rpmsign'.")

  digest_algo = options.key?(:digest_algo) ?  options[:digest_algo] : 'sha256'
  timeout_seconds = options.key?(:timeout_seconds) ?  options[:timeout_seconds] : 60
  verbose = options.key?(:verbose) ?  options[:verbose] : false

  gpgkey = load_key(gpg_keydir, verbose)

  gpg_sign_cmd_extra_args = nil
  if Gem::Version.new(Simp::RPM.version) >= Gem::Version.new('4.13.0')
    gpg_sign_cmd_extra_args = "--define '%_gpg_sign_cmd_extra_args --pinentry-mode loopback --verbose'"
  end

  signcommand = [
    'rpm',
    "--define '%_signature gpg'",
    "--define '%__gpg %{_bindir}/gpg'",
    "--define '%_gpg_name #{gpgkey[:name]}'",
    "--define '%_gpg_path #{gpgkey[:dir]}'",
    "--define '%_gpg_digest_algo #{digest_algo}'",
    gpg_sign_cmd_extra_args,
    "--resign #{rpm}"
  ].compact.join(' ')

  success = false
  begin
    if verbose
      puts "Signing #{rpm} with #{gpgkey[:name]} from #{gpgkey[:dir]}:\n  #{signcommand}"
    end

    require 'timeout'
    # With rpm-sign-4.14.2-11.el8_0 (EL 8.0), if rpm cannot start the
    # gpg-agent daemon, it will just hang. We need to be able to detect
    # the problem and report the failure.
    Timeout::timeout(timeout_seconds) do

      status = nil
      PTY.spawn(signcommand) do |read, write, pid|
        begin
          while !read.eof? do
            # rpm version >= 4.13.0 will stand up a gpg-agent and so the
            # prompt for the passphrase will only actually happen if this is
            # the first RPM to be signed with the key after the gpg-agent is
            # started and the key's passphrase has not been cleared from the
            # agent's cache.
            read.expect(/(pass\s?phrase:|verwrite).*/) do |text|
              if text.last.include?('verwrite')
                write.puts('y')
              else
                write.puts(gpgkey[:password])
              end

              write.flush
            end
          end
        rescue Errno::EIO
          # Will get here once input is no longer needed, which can be
          # immediately, if a gpg-agent is already running and the
          # passphrase for the key is loaded in its cache.
        end

        Process.wait(pid)
        status = $?
      end

      if status && !status.success?
        raise "Failure running <#{signcommand}>"
      end
    end

    puts "Successfully signed #{rpm}" if verbose
    success = true

  rescue Timeout::Error
    $stderr.puts "Failed to sign #{rpm} in #{timeout_seconds} seconds."
  rescue Exception => e
    $stderr.puts "Error occurred while attempting to sign #{rpm}:"
    $stderr.puts e
  end

  success
end

.sign_rpms(rpm_dir, gpg_keydir, options = {}) ⇒ Object

Signs any RPMs found within the entire rpm_dir directory tree with the GPG key found in gpg_keydir

Parameters:

  • rpm_dir

    A directory or directory glob pattern specifying 1 or more directories containing RPM files to sign.

  • gpg_keydir

    The full path of the directory where the key resides

  • options (defaults to: {})

    Options Hash

Returns:

  • Hash of RPM signing results or nil if no RPMs found in rpm_dir

    • Each Hash key is the path to a RPM

    • Each Hash value is the status of the signing operation: :signed, :unsigned, :skipped_already_signed

Raises:

  • RuntimeError if ‘rpmsign’ executable cannot be found, the ‘gpg’ executable cannot be found, the GPG key directory does not exist, the GPG key metadata cannot be determined via ‘gpg’ or any RPM signing operation failed



269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
# File 'lib/simp/rpm_signer.rb', line 269

def self.sign_rpms(rpm_dir, gpg_keydir, options = {})
 opts = {
   :digest_algo        => 'sha256',
   :force              => false,
   :max_concurrent     => 1,
   :progress_bar_title => 'sign_rpms',
   :timeout_seconds    => 60,
   :verbose            => false
  }.merge(options)

  rpm_dirs = Dir.glob(rpm_dir)
  to_sign = []

  rpm_dirs.each do |rpm_dir|
    Find.find(rpm_dir) do |rpm|
      next unless File.readable?(rpm)
      to_sign << rpm if rpm =~ /\.rpm$/
    end
  end

  return nil if to_sign.empty?

  results = []
  begin
    results = Parallel.map(
      to_sign,
      :in_processes => 1,
      :progress => opts[:progress_bar_title]
    ) do |rpm|
      _result = nil

      begin
        if opts[:force] || !Simp::RPM.new(rpm).signature
          _result = [ rpm, sign_rpm(rpm, gpg_keydir, opts) ]
          _result[1] = _result[1] ? :signed : :unsigned
        else
          puts "Skipping signed package #{rpm}" if opts[:verbose]
          _result = [ rpm, :skipped_already_signed ]
        end
      rescue Exception => e
        # can get here if rpm is malformed and Simp::RPM.new fails
        $stderr.puts "Failed to sign #{rpm}:\n#{e.message}"
        _result = [ rpm, :unsigned ]
      end

      _result
    end
  ensure
    kill_gpg_agent(gpg_keydir)
  end

  results.to_h
end