class Simp::RpmSigner

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

Public Class Methods

clear_gpg_keys_cache() click to toggle source
# File lib/simp/rpm_signer.rb, line 323
def self.clear_gpg_keys_cache
  @@gpg_keys.clear
end
kill_gpg_agent(gpg_keydir) click to toggle source

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.

# 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) click to toggle source

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.

@param gpg_keydir The full path of the directory where the key resides @param verbose Whether to log debug information.

@raise If the ‘gpg’ executable cannot be found, the GPG key directory

does not exist or GPG key metadata cannot be determined via 'gpg'
# 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={}) click to toggle source

Signs the given RPM with the GPG key found in gpg_keydir

@param rpm Fully qualified path to an RPM to be signed. @param gpg_keydir The full path of the directory where the key resides. @param options Options Hash

@options options :digest_algo Digest algorithm to use in RPM

signing operation; defaults to 'sha256'

@options options :timeout_seconds Timeout in seconds for an individual

RPM signing operation; defaults to 60.

@options options :verbose Whether to log debug information;

defaults to false.

@return Whether package signing operation succeeded @raise 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'
# 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 = {}) click to toggle source

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

@param rpm_dir A directory or directory glob pattern specifying 1 or

more directories containing RPM files to sign.

@param gpg_keydir The full path of the directory where the key resides @param options Options Hash

@options options :digest_algo Digest algorithm to use in RPM

signing operation; defaults to
'sha256'

@options options :force Force RPMs that are already signed

to be resigned; defaults to false.

@options options :max_concurrent Maximum number of concurrent RPM

signing operations; defaults to 1.

@options options :progress_bar_title Title for the progress bar logged to

the console during the signing process;
defaults to 'sign_rpms'.

@options options :timeout_seconds Timeout in seconds for an individual

RPM signing operation; defauls to 60.

@options options :verbose Whether to log debug information;

defaults to false.

@return 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

@raise 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
# 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