(MODULES-4686) fix incorrect GPG keys parsing on Debian 9
[puppet-modules/puppetlabs-apt.git] / lib / puppet / provider / apt_key / apt_key.rb
index 67a8aa0643db68edb8fca0341262c5325b38bc4e..3f95c3cc96af9c68f40234d6f07a9a3072be5173 100644 (file)
@@ -1,4 +1,3 @@
-require 'date'
 require 'open-uri'
 require 'net/ftp'
 require 'tempfile'
@@ -16,9 +15,10 @@ Puppet::Type.type(:apt_key).provide(:apt_key) do
   confine    :osfamily => :debian
   defaultfor :osfamily => :debian
   commands   :apt_key  => 'apt-key'
+  commands   :gpg      => '/usr/bin/gpg'
 
   def self.instances
-    cli_args = ['adv','--list-keys', '--with-colons', '--fingerprint']
+    cli_args = ['adv','--list-keys', '--with-colons', '--fingerprint', '--fixed-list-mode']
 
     if RUBY_VERSION > '1.8.7'
       key_output = apt_key(cli_args).encode('UTF-8', 'binary', :invalid => :replace, :undef => :replace, :replace => '')
@@ -26,15 +26,25 @@ Puppet::Type.type(:apt_key).provide(:apt_key) do
       key_output = apt_key(cli_args)
     end
 
-    pub_line, fpr_line = nil
+    pub_line, sub_line, fpr_line = nil
 
     key_array = key_output.split("\n").collect do |line|
       if line.start_with?('pub')
           pub_line = line
+          # reset fpr_line, to skip any previous subkeys which were collected
+          fpr_line = nil
+          sub_line = nil
+      elsif line.start_with?('sub')
+          sub_line = line
       elsif line.start_with?('fpr')
           fpr_line = line
       end
 
+      if (sub_line and fpr_line)
+        sub_line, fpr_line = nil
+        next
+      end
+
       next unless (pub_line and fpr_line)
 
       line_hash = key_line_hash(pub_line, fpr_line)
@@ -45,7 +55,7 @@ Puppet::Type.type(:apt_key).provide(:apt_key) do
       expired = false
 
       if line_hash[:key_expiry]
-        expired = Date.today > Date.parse(line_hash[:key_expiry])
+        expired = Time.now >= line_hash[:key_expiry]
       end
 
       new(
@@ -56,10 +66,10 @@ Puppet::Type.type(:apt_key).provide(:apt_key) do
         :long        => line_hash[:key_long],
         :ensure      => :present,
         :expired     => expired,
-        :expiry      => line_hash[:key_expiry],
+        :expiry      => line_hash[:key_expiry].nil? ? nil : line_hash[:key_expiry].strftime("%Y-%m-%d"),
         :size        => line_hash[:key_size],
         :type        => line_hash[:key_type],
-        :created     => line_hash[:key_created]
+        :created     => line_hash[:key_created].strftime("%Y-%m-%d")
       )
     end
     key_array.compact!
@@ -95,8 +105,8 @@ Puppet::Type.type(:apt_key).provide(:apt_key) do
       :key_short       => fingerprint[-8..-1], # last 8 characters of fingerprint
       :key_size        => pub_split[2],
       :key_type        => nil,
-      :key_created     => pub_split[5],
-      :key_expiry      => pub_split[6].empty? ? nil : pub_split[6],
+      :key_created     => Time.at(pub_split[5].to_i),
+      :key_expiry      => pub_split[6].empty? ? nil : Time.at(pub_split[6].to_i),
     }
 
     # set key type based on types defined in /usr/share/doc/gnupg/DETAILS.gz
@@ -118,10 +128,24 @@ Puppet::Type.type(:apt_key).provide(:apt_key) do
     parsedValue = URI::parse(value)
     if parsedValue.scheme.nil?
       fail("The file #{value} does not exist") unless File.exists?(value)
-      value
+      # Because the tempfile method has to return a live object to prevent GC
+      # of the underlying file from occuring too early, we also have to return
+      # a file object here.  The caller can still call the #path method on the
+      # closed file handle to get the path.
+      f = File.open(value, 'r')
+      f.close
+      f
     else
       begin
-        key = parsedValue.read
+        # Only send basic auth if URL contains userinfo
+        # Some webservers (e.g. Amazon S3) return code 400 if empty basic auth is sent
+        if parsedValue.userinfo.nil?
+          key = parsedValue.read
+        else
+          user_pass = parsedValue.userinfo.split(':')
+          parsedValue.userinfo = ''
+          key = open(parsedValue, :http_basic_authentication => user_pass).read
+        end
       rescue OpenURI::HTTPError, Net::FTPPermError => e
         fail("#{e.message} for #{resource[:source]}")
       rescue SocketError
@@ -132,11 +156,33 @@ Puppet::Type.type(:apt_key).provide(:apt_key) do
     end
   end
 
+  # The tempfile method needs to return the tempfile object to the caller, so
+  # that it doesn't get deleted by the GC immediately after it returns.  We
+  # want the caller to control when it goes out of scope.
   def tempfile(content)
     file = Tempfile.new('apt_key')
     file.write content
     file.close
-    file.path
+    #confirm that the fingerprint from the file, matches the long key that is in the manifest
+    if name.size == 40
+      if File.executable? command(:gpg)
+        extracted_key = execute(["#{command(:gpg)} --with-fingerprint --with-colons #{file.path} | awk -F: '/^fpr:/ { print $10 }'"], :failonfail => false)
+        extracted_key = extracted_key.chomp
+
+        found_match = false
+        extracted_key.each_line do |line|
+          if line.chomp == name
+            found_match = true
+          end
+        end
+        if not found_match
+          fail("The id in your manifest #{resource[:name]} and the fingerprint from content/source do not match. Please check there is not an error in the id or check the content/source is legitimate.")
+        end
+      else
+        warning('/usr/bin/gpg cannot be found for verification of the id.')
+      end
+    end
+    file
   end
 
   def exists?
@@ -149,14 +195,16 @@ Puppet::Type.type(:apt_key).provide(:apt_key) do
       # Breaking up the command like this is needed because it blows up
       # if --recv-keys isn't the last argument.
       command.push('adv', '--keyserver', resource[:server])
-      unless resource[:keyserver_options].nil?
-        command.push('--keyserver-options', resource[:keyserver_options])
+      unless resource[:options].nil?
+        command.push('--keyserver-options', resource[:options])
       end
       command.push('--recv-keys', resource[:id])
     elsif resource[:content]
-      command.push('add', tempfile(resource[:content]))
+      key_file = tempfile(resource[:content])
+      command.push('add', key_file.path)
     elsif resource[:source]
-      command.push('add', source_to_file(resource[:source]))
+      key_file = source_to_file(resource[:source])
+      command.push('add', key_file.path)
     # In case we really screwed up, better safe than sorry.
     else
       fail("an unexpected condition occurred while trying to add the key: #{resource[:id]}")