diff --git a/lib/puppet/provider/java_ks/keytool.rb b/lib/puppet/provider/java_ks/keytool.rb index 953f226..e335beb 100644 --- a/lib/puppet/provider/java_ks/keytool.rb +++ b/lib/puppet/provider/java_ks/keytool.rb @@ -1,373 +1,375 @@ +# frozen_string_literal: true + require 'openssl' require 'timeout' require 'puppet/util/filetype' Puppet::Type.type(:java_ks).provide(:keytool) do desc 'Uses a combination of openssl and keytool to manage Java keystores' def command_keytool 'keytool' end # Keytool can only import a keystore if the format is pkcs12. Generating and # importing a keystore is used to add private_key and certificate pairs. def to_pkcs12(path) case private_key_type when :rsa pkey = OpenSSL::PKey::RSA.new File.read(private_key), password when :dsa pkey = OpenSSL::PKey::DSA.new File.read(private_key), password when :ec pkey = OpenSSL::PKey::EC.new File.read(private_key), password end if chain x509_cert = OpenSSL::X509::Certificate.new File.read certificate chain_certs = get_chain(chain) else chain_certs = get_chain(certificate) x509_cert = chain_certs.shift end pkcs12 = OpenSSL::PKCS12.create(password, @resource[:name], pkey, x509_cert, chain_certs) File.open(path, 'wb') { |f| f.print pkcs12.to_der } end # Keytool can only import a jceks keystore if the format is der. Generating and # importing a keystore is used to add private_key and certificate pairs. def to_der(path) x509_cert = OpenSSL::X509::Certificate.new File.read certificate File.open(path, 'wb') { |f| f.print x509_cert.to_der } end def get_chain(path) chain_certs = File.read(path, encoding: 'ISO-8859-1').scan(%r{-----BEGIN [^\n]*CERTIFICATE.*?-----END [^\n]*CERTIFICATE-----}m) if chain_certs.any? chain_certs.map { |cert| OpenSSL::X509::Certificate.new cert } else chain_certs << ((OpenSSL::X509::Certificate.new File.binread path)) end end def password if @resource[:password_file].nil? @resource[:password] else file = File.open(@resource[:password_file], 'r') pword = file.read file.close pword.chomp end end def password_file pword = password source_pword = sourcepassword tmpfile = Tempfile.new("#{@resource[:name]}.") contents = if File.exist?(@resource[:target]) && !File.zero?(@resource[:target]) if !source_pword.nil? "#{pword}\n#{source_pword}" else "#{pword}\n#{pword}" end elsif !source_pword.nil? "#{pword}\n#{pword}\n#{source_pword}" else "#{pword}\n#{pword}\n#{pword}" end tmpfile.write(contents) tmpfile.flush tmpfile end # Where we actually to the import of the file created using to_pkcs12. def import_ks tmppk12 = Tempfile.new("#{@resource[:name]}.") to_pkcs12(tmppk12.path) cmd = [ command_keytool, '-importkeystore', '-srcstoretype', 'PKCS12', '-destkeystore', @resource[:target], '-srckeystore', tmppk12.path, '-alias', @resource[:name] ] cmd << '-trustcacerts' if @resource[:trustcacerts] == :true cmd += ['-destkeypass', @resource[:destkeypass]] unless @resource[:destkeypass].nil? pwfile = password_file run_command(cmd, @resource[:target], pwfile) tmppk12.close! pwfile.close! if pwfile.is_a? Tempfile end def import_pkcs12 cmd = [ command_keytool, '-importkeystore', '-srcstoretype', 'PKCS12', '-destkeystore', @resource[:target], '-srckeystore', certificate ] if @resource[:source_alias] cmd.concat([ '-srcalias', @resource[:source_alias], '-destalias', @resource[:name] ]) end if @resource[:destkeypass] cmd.concat([ '-destkeypass', @resource[:destkeypass] ]) end pwfile = password_file run_command(cmd, @resource[:target], pwfile) pwfile.close! if pwfile.is_a? Tempfile end def import_jceks tmpder = Tempfile.new("#{@resource[:name]}.") to_der(tmpder.path) cmd = [ command_keytool, '-importcert', '-noprompt', '-alias', @resource[:name], '-file', tmpder.path, '-keystore', @resource[:target], '-storetype', storetype ] cmd << '-trustcacerts' if @resource[:trustcacerts] == :true cmd += ['-destkeypass', @resource[:destkeypass]] unless @resource[:destkeypass].nil? pwfile = password_file run_command(cmd, @resource[:target], pwfile) pwfile.close! if pwfile.is_a? Tempfile end def exists? cmd = [ command_keytool, '-list', '-keystore', @resource[:target], '-alias', @resource[:name] ] cmd += ['-storetype', storetype] if storetype == :jceks begin tmpfile = password_file run_command(cmd, false, tmpfile) tmpfile.close! return true rescue => e - if e.message =~ %r{password was incorrect}i + if e.message.match?(%r{password was incorrect}i) # we have the wrong password for the keystore. so delete it if :password_fail_reset if @resource[:password_fail_reset] == :true File.delete(@resource[:target]) end end return false end end # Extracts the fingerprints of a given output def extract_fingerprint(output) output.scan(%r{Certificate fingerprints:\n\s+(?:MD5: .*\n\s+)?SHA1: (.*)}).flatten.join('/') end # Reading the fingerprint of the certificate on disk. def latest # The certificate file may not exist during a puppet noop run as it's managed by puppet. # Return value must be different to provider.current to signify a possible trigger event. if Puppet[:noop] && !File.exist?(certificate) 'latest' elsif storetype == :pkcs12 cmd = [ command_keytool, '-v', '-list', '-keystore', certificate, '-storetype', 'PKCS12', '-storepass', sourcepassword ] output = run_command(cmd) latest = extract_fingerprint(output) latest else cmd = [ command_keytool, '-v', '-printcert', '-file', certificate ] output = run_command(cmd) if chain cmd = [ command_keytool, '-v', '-printcert', '-file', chain ] output += run_command(cmd) end latest = extract_fingerprint(output) latest end end # Reading the fingerprint of the certificate currently in the keystore. def current # The keystore file may not exist during a puppet noop run as it's managed by puppet. if Puppet[:noop] && !File.exist?(@resource[:target]) 'current' else cmd = [ command_keytool, '-list', '-v', '-keystore', @resource[:target], '-alias', @resource[:name] ] cmd += ['-storetype', storetype] if storetype == :jceks tmpfile = password_file output = run_command(cmd, false, tmpfile) tmpfile.close! current = extract_fingerprint(output) current end end # Determine if we need to do an import of a private_key and certificate pair # or just add a signed certificate, then do it. def create if !certificate.nil? && !private_key.nil? import_ks elsif certificate.nil? && !private_key.nil? raise Puppet::Error, 'Keytool is not capable of importing a private key without an accompanying certificate.' elsif storetype == :jceks import_jceks elsif storetype == :pkcs12 import_pkcs12 else cmd = [ command_keytool, '-importcert', '-noprompt', '-alias', @resource[:name], '-file', certificate, '-keystore', @resource[:target] ] cmd << '-trustcacerts' if @resource[:trustcacerts] == :true tmpfile = password_file run_command(cmd, @resource[:target], tmpfile) tmpfile.close! end end def destroy cmd = [ command_keytool, '-delete', '-alias', @resource[:name], '-keystore', @resource[:target] ] cmd += ['-storetype', storetype] if storetype == :jceks tmpfile = password_file run_command(cmd, false, tmpfile) tmpfile.close! end # Being safe since I have seen some additions overwrite and some just throw errors. def update destroy create end def certificate @resource[:certificate] end def private_key @resource[:private_key] end def private_key_type @resource[:private_key_type] end def chain @resource[:chain] end def sourcepassword @resource[:source_password] end def storetype @resource[:storetype] end def run_command(cmd, target = false, stdinfile = false, env = {}) env[:PATH] = @resource[:path].join(File::PATH_SEPARATOR) if resource[:path] # The Puppet::Util::Execution.execute method is deprecated in Puppet 3.x # but we need this to work on 2.7.x too. exec_method = if Puppet::Util::Execution.respond_to?(:execute) Puppet::Util::Execution.method(:execute) else Puppet::Util.method(:execute) end withenv = if Puppet::Util::Execution.respond_to?(:withenv) Puppet::Util::Execution.method(:withenv) else Puppet::Util.method(:withenv) end # the java keytool will not correctly deal with an empty target keystore # file. If we encounter an empty keystore target file, preserve the mode, # owner and group, temporarily raise the umask, and delete the empty file. if target && (File.exist?(target) && File.zero?(target)) stat = File.stat(target) umask = File.umask(0o077) File.delete(target) end # There's a problem in IBM java keytool wherein stdin cannot be used # (trivially) to pass in the keystore passwords. The below hack makes the # provider work on SLES with minimal effort at the cost of letting the # passphrase to the keystore show up in the process list as an argument. # From a best practice standpoint the keystore should be protected by file # permissions and not just the passphrase so "making it work on SLES" # trumps. if Facter.value('osfamily') == 'Suse' && @resource[:password] cmd_to_run = cmd.is_a?(String) ? cmd.split(%r{\s}).first : cmd.first if cmd_to_run == command_keytool cmd << '-srcstorepass' << @resource[:password] cmd << '-deststorepass' << @resource[:password] end end # Now run the command options = { failonfail: true, combine: true } output = nil begin Timeout.timeout(@resource[:keytool_timeout], Timeout::Error) do output = if stdinfile withenv.call(env) do exec_method.call(cmd, options.merge(stdinfile: stdinfile.path)) end else withenv.call(env) do exec_method.call(cmd, options) end end end rescue Timeout::Error raise Puppet::Error, "Timed out waiting for '#{@resource[:name]}' to run keytool" end # for previously empty files, restore the umask, mode, owner and group. # The funky double-take check is because on Suse defined? doesn't seem # to behave quite the same as on Debian, RedHat if target and (defined? stat and stat) # rubocop:disable Style/AndOr : Changing 'and' to '&&' causes test failures. File.umask(umask) # Need to change group ownership before mode to prevent making the file # accessible to the wrong group. File.chown(stat.uid, stat.gid, target) File.chmod(stat.mode, target) end output end end diff --git a/lib/puppet/type/java_ks.rb b/lib/puppet/type/java_ks.rb index be15cf3..26c28b9 100644 --- a/lib/puppet/type/java_ks.rb +++ b/lib/puppet/type/java_ks.rb @@ -1,222 +1,224 @@ +# frozen_string_literal: true + Puppet::Type.newtype(:java_ks) do @doc = 'Manages the entries in a java keystore, and uses composite namevars to accomplish the same alias spread across multiple target keystores.' ensurable do desc 'Has three states: present, absent, and latest. Latest will compare the on disk SHA1 fingerprint of the certificate to that in keytool to determine if insync? returns true or false. We redefine insync? for this parameter to accomplish this.' newvalue(:present) do provider.create end newvalue(:absent) do provider.destroy end newvalue(:latest) do if provider.exists? provider.update else provider.create end end def insync?(is) @should.each do |should| case should when :present return true if is == :present when :absent return true if is == :absent when :latest unless is == :absent return true if provider.latest.include? provider.current end end end false end defaultto :present end newparam(:name) do desc 'The alias that is used to identify the entry in the keystore. This will be converted to lowercase.' isnamevar munge do |value| value.downcase end end newparam(:target) do desc 'Destination file for the keystore. This will autorequire the parent directory of the file.' isnamevar end newparam(:certificate) do desc 'A server certificate, followed by zero or more intermediate certificate authorities. All certificates will be placed in the keystore. This will autorequire the specified file.' isrequired end newparam(:storetype) do desc 'Optional storetype Valid options: , , ' newvalues(:jceks, :pkcs12, :jks) end newparam(:private_key) do desc 'If you want an application to be a server and encrypt traffic, you will need a private key. Private key entries in a keystore must be accompanied by a signed certificate for the keytool provider. This will autorequire the specified file.' end newparam(:private_key_type) do desc 'The type of the private key. Usually the private key is of type RSA key but it can also be an Elliptic Curve key (EC) or DSA. Valid options: , , . Defaults to ' newvalues(:rsa, :dsa, :ec) defaultto :rsa end newparam(:chain) do desc 'The intermediate certificate authorities, if they are to be taken from a file separate from the server certificate. This will autorequire the specified file.' end newparam(:password) do desc 'The password used to protect the keystore. If private keys are subsequently also protected this password will be used to attempt unlocking. Must be six or more characters in length. Cannot be used together with :password_file, but you must pass at least one of these parameters.' validate do |value| raise Puppet::Error, "password is #{value.length} characters long; must be 6 characters or greater in length" if value.length < 6 end end newparam(:password_file) do desc 'The path to a file containing the password used to protect the keystore. This cannot be used together with :password, but you must pass at least one of these parameters.' end newparam(:password_fail_reset) do desc "If the supplied password does not succeed in unlocking the keystore file, then delete the keystore file and create a new one. Default: false." newvalues(:true, :false) defaultto :false end newparam(:destkeypass) do desc 'The password used to protect the key in keystore.' validate do |value| raise Puppet::Error, "destkeypass is #{value.length} characters long; must be of length 6 or greater" if value.length < 6 end end newparam(:trustcacerts) do desc "Certificate authorities aren't by default trusted so if you are adding a CA you need to set this to true. Defaults to :false." newvalues(:true, :false) defaultto :false end newparam(:path) do desc "The search path used for command (keytool, openssl) execution. Paths can be specified as an array or as a '#{File::PATH_SEPARATOR}' separated list." # Support both arrays and colon-separated fields. def value=(*values) @value = values.flatten.map { |val| val.split(File::PATH_SEPARATOR) }.flatten end end newparam(:keytool_timeout) do desc 'Timeout for the keytool command in seconds.' defaultto 120 end newparam(:source_password) do desc 'The source keystore password' end newparam(:source_alias) do desc 'The source certificate alias' end # Where we setup autorequires. autorequire(:file) do auto_requires = [] [:private_key, :certificate, :chain].each do |param| if @parameters.include?(param) auto_requires << @parameters[param].value end end if @parameters.include?(:target) auto_requires << ::File.dirname(@parameters[:target].value) end auto_requires end # Our title_patterns method for mapping titles to namevars for supporting # composite namevars. def self.title_patterns [ [ %r{^([^:]+)$}, [ [:name], ], ], [ %r{^(.*):([a-z]:(/|\\).*)$}i, [ [:name], [:target], ], ], [ %r{^(.*):(.*)$}, [ [:name], [:target], ], ], ] end validate do if value(:password) && value(:password_file) raise Puppet::Error, "You must pass either 'password' or 'password_file', not both." end unless value(:password) || value(:password_file) raise Puppet::Error, "You must pass one of 'password' or 'password_file'." end if value(:storetype) == :pkcs12 && value(:source_password).nil? fail "You must provide 'source_password' when using a 'pkcs12' storetype." # rubocop:disable Style/SignalException : Associated test fails if 'raise' is used end end end diff --git a/spec/acceptance/chain_key_spec.rb b/spec/acceptance/chain_key_spec.rb index 8057a9a..4894bc6 100644 --- a/spec/acceptance/chain_key_spec.rb +++ b/spec/acceptance/chain_key_spec.rb @@ -1,151 +1,153 @@ +# frozen_string_literal: true + require 'spec_helper_acceptance' describe 'managing intermediate certificates' do # rubocop:disable RSpec/InstanceVariable : Instance variables are inherited and thus cannot be contained within lets describe 'managing combined and seperate java chain keys' do include_context 'common variables' it 'creates two private key with chain certs' do pp = <<-MANIFEST java_ks { 'combined.example.com:#{@temp_dir}chain_combined_key.ks': ensure => latest, certificate => "#{@temp_dir}leafchain.pem", private_key => "#{@temp_dir}leafkey.pem", password => 'puppet', path => #{@resource_path}, } java_ks { 'seperate.example.com:#{@temp_dir}chain_key.ks': ensure => latest, certificate => "#{@temp_dir}leaf.pem", chain => "#{@temp_dir}chain.pem", private_key => "#{@temp_dir}leafkey.pem", password => 'puppet', path => #{@resource_path}, } MANIFEST idempotent_apply(pp) end expectations_combined = [ %r{Alias name: combined\.example\.com}, %r{Entry type: (keyEntry|PrivateKeyEntry)}, %r{Certificate chain length: 3}, %r{^Serial number: 5.*^Serial number: 4.*^Serial number: 3}m, ] it 'verifies the private key #combined' do run_shell(keytool_command("-list -v -keystore #{@temp_dir}chain_combined_key.ks -storepass puppet"), expect_failures: true) do |r| expectations_combined.each do |expect| expect(r.stdout).to match(expect) end end end expectations_seperate = [ %r{Alias name: seperate\.example\.com}, %r{Entry type: (keyEntry|PrivateKeyEntry)}, %r{Certificate chain length: 3}, %r{^Serial number: 5.*^Serial number: 4.*^Serial number: 3}m, ] it 'verifies the private key #seperate' do run_shell(keytool_command("-list -v -keystore #{@temp_dir}chain_key.ks -storepass puppet"), expect_failures: true) do |r| expectations_seperate.each do |expect| expect(r.stdout).to match(expect) end end end it 'updates the two key chains' do pp = <<-MANIFEST java_ks { 'combined.example.com:#{@temp_dir}chain_combined_key.ks': ensure => latest, certificate => "#{@temp_dir}leafchain2.pem", private_key => "#{@temp_dir}leafkey.pem", password => 'puppet', path => #{@resource_path}, } java_ks { 'seperate.example.com:#{@temp_dir}chain_key.ks': ensure => latest, certificate => "#{@temp_dir}leaf.pem", chain => "#{@temp_dir}chain2.pem", private_key => "#{@temp_dir}leafkey.pem", password => 'puppet', path => #{@resource_path}, } MANIFEST idempotent_apply(pp) expectations_combined = [ %r{Alias name: combined\.example\.com}, %r{Entry type: (keyEntry|PrivateKeyEntry)}, %r{Certificate chain length: 2}, %r{^Serial number: 5.*^Serial number: 6}m, ] run_shell(keytool_command("-list -v -keystore #{@temp_dir}chain_combined_key.ks -storepass puppet"), expect_failures: true) do |r| expectations_combined.each do |expect| expect(r.stdout).to match(expect) end end expectations_seperate = [ %r{Alias name: seperate\.example\.com}, %r{Entry type: (keyEntry|PrivateKeyEntry)}, %r{Certificate chain length: 2}, %r{^Serial number: 5.*Serial number: 6}m, ] run_shell(keytool_command("-list -v -keystore #{@temp_dir}chain_key.ks -storepass puppet"), expect_failures: true) do |r| expectations_seperate.each do |expect| expect(r.stdout).to match(expect) end end end end describe 'managing non existent java chain keys in noop' do include_context 'common variables' it 'does not create a new keystore in noop' do pp = <<-MANIFEST $filenames = ["#{@temp_dir}noop_ca.pem", "#{@temp_dir}noop_chain.pem", "#{@temp_dir}noop_privkey.pem"] file { $filenames: ensure => file, content => 'content', } -> java_ks { 'broker.example.com:#{@temp_dir}noop_chain_key.ks': ensure => latest, certificate => "#{@temp_dir}noop_ca.pem", chain => "#{@temp_dir}noop_chain.pem", private_key => "#{@temp_dir}noop_privkey.pem", password => 'puppet', path => #{@resource_path}, } MANIFEST # in noop mode, when the dependent certificate files are not present in the system, # java_ks will not invoke openssl to validate their status, thus noop will succeed apply_manifest(pp, noop: true) end # verifies the dependent files are missing ['noop_ca.pem', 'noop_chain.pem', 'noop_privkey.pem'].each do |filename| describe filename do it "doesn't exist" do result = remote_file_exists?("#{@temp_dir}#{filename}") expect(result.exit_code).to be(1) end end end # verifies the keystore is not created describe 'noop_chain_key.ks' do it "doesn't exist" do result = remote_file_exists?("#{@temp_dir}noop_chain_key.ks") expect(result.exit_code).to be(1) end end end end diff --git a/spec/acceptance/destkeypass_spec.rb b/spec/acceptance/destkeypass_spec.rb index 9034b00..65eee90 100644 --- a/spec/acceptance/destkeypass_spec.rb +++ b/spec/acceptance/destkeypass_spec.rb @@ -1,35 +1,37 @@ +# frozen_string_literal: true + require 'spec_helper_acceptance' describe 'password protected java private keys', unless: UNSUPPORTED_PLATFORMS.include?(os[:family]) do # rubocop:disable RSpec/InstanceVariable : Instance variables are inherited and thus cannot be contained within lets include_context 'common variables' target = "#{@target_dir}destkeypass.ks" it 'creates a password protected private key' do pp = <<-MANIFEST java_ks { 'broker.example.com:#{@temp_dir}#{target}': ensure => latest, certificate => "#{@temp_dir}ca.pem", private_key => "#{@temp_dir}privkey.pem", password => 'testpass', destkeypass => 'testkeypass', path => #{@resource_path}, } MANIFEST idempotent_apply(pp) end it 'can make a cert req with the right password' do run_shell(keytool_command('-certreq -alias broker.example.com -v '\ "-keystore #{@temp_dir}#{target} -storepass testpass -keypass testkeypass"), expect_failures: true) do |r| expect(r.stdout).to match(%r{-BEGIN NEW CERTIFICATE REQUEST-}) end end it 'cannot make a cert req with the wrong password' do result = run_shell(keytool_command('-certreq -alias broker.example.com -v '\ "-keystore #{@temp_dir}#{target} -storepass qwert -keypass qwert"), expect_failures: true) expect(result.stdout).to match(%r{keytool error}) end end diff --git a/spec/acceptance/keystore_spec.rb b/spec/acceptance/keystore_spec.rb index 02c5f53..baf426f 100644 --- a/spec/acceptance/keystore_spec.rb +++ b/spec/acceptance/keystore_spec.rb @@ -1,193 +1,195 @@ +# frozen_string_literal: true + require 'spec_helper_acceptance' describe 'managing java keystores' do # rubocop:disable RSpec/InstanceVariable : Instance variables are inherited and thus cannot be contained within lets include_context 'common variables' describe 'basic tests' do it 'creates a keystore' do command = "rm #{@temp_dir}keystore.ks" command = interpolate_powershell(command) if os[:family] == 'windows' run_shell(command, expect_failures: true) pp_one = <<-MANIFEST java_ks { 'puppetca:keystore': ensure => latest, certificate => "#{@temp_dir}ca.pem", target => '#{@temp_dir}keystore.ks', password => 'puppet', trustcacerts => true, path => #{@resource_path}, } MANIFEST idempotent_apply(pp_one) end expectations = [ %r{Your keystore contains 1 entry}, %r{Alias name: puppetca}, %r{CN=Test CA}, ] it 'verifies the keytore' do run_shell(keytool_command("-list -v -keystore #{@temp_dir}keystore.ks -storepass puppet")) do |r| expect(r.exit_code).to be_zero expectations.each do |expect| expect(r.stdout).to match(expect) end end end it 'uses password_file' do pp_two = <<-MANIFEST file { '#{@temp_dir}password': ensure => file, content => 'puppet', } java_ks { 'puppetca2:keystore': ensure => latest, certificate => "#{@temp_dir}ca2.pem", target => '#{@temp_dir}keystore.ks', password_file => '#{@temp_dir}password', trustcacerts => true, path => #{@resource_path}, require => File['#{@temp_dir}password'] } MANIFEST idempotent_apply(pp_two) end it 'recreates a keystore if password fails' do pp_three = <<-MANIFEST java_ks { 'puppetca:#{@temp_dir}keystore': ensure => latest, certificate => "#{@temp_dir}ca.pem", target => '#{@temp_dir}keystore.ks', password => 'pepput', password_fail_reset => true, trustcacerts => true, path => #{@resource_path}, } MANIFEST idempotent_apply(pp_three) end it 'verifies the keystore again' do run_shell(keytool_command("-list -v -keystore #{@temp_dir}keystore.ks -storepass pepput")) do |r| expect(r.exit_code).to be_zero expectations.each do |expect| expect(r.stdout).to match(expect) end end end end unless os[:family] == 'ubuntu' && os[:release].start_with?('18.04') describe 'storetype' do it 'creates a keystore' do pp = <<-MANIFEST java_ks { 'puppetca:#{@temp_dir}keystore': ensure => latest, certificate => "#{@temp_dir}ca.pem", target => '#{@temp_dir}keystore.ks', password => 'pepput', trustcacerts => true, path => #{@resource_path}, storetype => 'jks', } MANIFEST idempotent_apply(pp) end expectations = [ %r{Your keystore contains 1 entry}, %r{Alias name: puppetca}, %r{CN=Test CA}, ] it 'verifies the keytore' do run_shell(keytool_command("-list -v -keystore #{@temp_dir}keystore.ks -storepass pepput")) do |r| expect(r.exit_code).to be_zero expectations.each do |expect| expect(r.stdout).to match(expect) end end end end end describe 'with der certificates' do it 'creates a keystore' do command = "rm #{@temp_dir}keystore.ks" command = interpolate_powershell(command) if os[:family] == 'windows' run_shell(command, expect_failures: true) pp_one = <<-MANIFEST java_ks { 'puppetcader:keystore': ensure => latest, certificate => "#{@temp_dir}ca.der", target => '#{@temp_dir}keystore.ks', password => 'puppet', trustcacerts => true, path => #{@resource_path}, } MANIFEST idempotent_apply(pp_one) end it 'adds a certificate and key' do pp_two = <<-MANIFEST java_ks { 'puppetcader_privkey:keystore': ensure => latest, certificate => "#{@temp_dir}ca.der", private_key => "#{@temp_dir}privkey.pem", target => '#{@temp_dir}keystore.ks', password => 'puppet', trustcacerts => true, path => #{@resource_path}, } MANIFEST idempotent_apply(pp_two) end expectations = [ %r{Your keystore contains 2 entries}, %r{Alias name: puppetcader}, %r{Alias name: puppetcader_privkey}, %r{CN=Test CA}, ] context 'when running on Linux', unless: os[:family] == 'windows' do it 'verifies the keystore' do run_shell(keytool_command("-list -v -keystore #{@temp_dir}keystore.ks -storepass puppet")) do |r| expect(r.exit_code).to be_zero expectations.each do |expect| expect(r.stdout).to match(expect) end end end end # On Windows, the keystore command warns about using a proprietary format when using DER formatted certs. We should # not take this as a failure, but also, we should also not blindly ignore all STDERR from the result either. If we # get an exit code of 1, we'll check to see if the STDERR message was the cert format warning and still pass the # test. We will still catch any errors that occur and are not related context 'when running on Windows', if: os[:family] == 'windows' do it 'verifies the keystore' do run_shell(keytool_command("-list -v -keystore #{@temp_dir}keystore.ks -storepass puppet"), expect_failures: true) do |r| expect(r.exit_code).to be_between(0, 1) expectations.each do |expect| expect(r.stdout).to match(expect) end # Pattern below ensures that it's the only warning printed out by anchoring to the end of line ($). This is to # handle the case that multiple warnings should ever occur - a looser match could potentially hide additional # errors expect(r.stderr.chomp).to match(%r{The JKS keystore.*pkcs12"\.$}) if r.exit_code == 1 end end end end end diff --git a/spec/acceptance/pkcs12_spec.rb b/spec/acceptance/pkcs12_spec.rb index 7c5d9dd..d4c8bbd 100644 --- a/spec/acceptance/pkcs12_spec.rb +++ b/spec/acceptance/pkcs12_spec.rb @@ -1,147 +1,149 @@ +# frozen_string_literal: true + require 'spec_helper_acceptance' # SLES by default does not support this form of encyrption. describe 'managing java pkcs12', unless: (os[:family] == 'sles' || (os[:family] == 'debian' && os[:release].start_with?('10')) || (os[:family] == 'ubuntu' && os[:release].start_with?('18'))) do # rubocop:disable RSpec/InstanceVariable : Instance variables are inherited and thus cannot be contained within lets include_context 'common variables' context 'with defaults' do it 'creates a private key with chain' do pp = <<-MANIFEST java_ks { 'Leaf Cert:#{@temp_dir}pkcs12.ks': ensure => #{@ensure_ks}, certificate => "#{@temp_dir}leaf.p12", storetype => 'pkcs12', password => 'puppet', path => #{@resource_path}, source_password => 'pkcs12pass' } MANIFEST idempotent_apply(pp) end expectations = [ %r{Alias name: leaf cert}, %r{Entry type: (keyEntry|PrivateKeyEntry)}, %r{Certificate chain length: 3}, %r{^Serial number: 5.*^Serial number: 4.*^Serial number: 3}m, ] it 'verifies the private key and chain' do run_shell(keytool_command("-list -v -keystore #{@temp_dir}pkcs12.ks -storepass puppet"), expect_failures: true) do |r| expectations.each do |expect| expect(r.stdout).to match(expect) end end end it 'updates the chain' do pp = <<-MANIFEST java_ks { 'Leaf Cert:#{@temp_dir}pkcs12.ks': ensure => #{@ensure_ks}, certificate => "#{@temp_dir}leaf2.p12", storetype => 'pkcs12', password => 'puppet', path => #{@resource_path}, source_password => 'pkcs12pass' } MANIFEST idempotent_apply(pp) expectations = if os[:family] == 'windows' || (os[:family] == 'ubuntu' && os[:release] == '20.04') [ %r{Alias name: leaf cert}, %r{Entry type: (keyEntry|PrivateKeyEntry)}, %r{Certificate chain length: 3}, %r{^Serial number: 3}m, %r{^Serial number: 4}m, %r{^Serial number: 5}m, ] else [ %r{Alias name: leaf cert}, %r{Entry type: (keyEntry|PrivateKeyEntry)}, %r{Certificate chain length: 2}, %r{^Serial number: 5$.*^Serial number: 6$}m, ] end run_shell(keytool_command("-list -v -keystore #{@temp_dir}pkcs12.ks -storepass puppet"), expect_failures: true) do |r| expectations.each do |expect| expect(r.stdout).to match(expect) end end end end # context 'with defaults' context 'with a different alias' do it 'creates a private key with chain' do pp = <<-MANIFEST java_ks { 'Leaf_Cert:#{@temp_dir}pkcs12.ks': ensure => #{@ensure_ks}, certificate => "#{@temp_dir}leaf.p12", storetype => 'pkcs12', password => 'puppet', path => #{@resource_path}, source_password => 'pkcs12pass', source_alias => 'Leaf Cert' } MANIFEST idempotent_apply(pp) end expectations = [ %r{Alias name: leaf_cert}, %r{Entry type: (keyEntry|PrivateKeyEntry)}, %r{Certificate chain length: 3}, %r{^Serial number: 5.*^Serial number: 4.*^Serial number: 3}m, ] it 'verifies the private key and chain' do run_shell(keytool_command("-list -v -keystore #{@temp_dir}pkcs12.ks -storepass puppet"), expect_failures: true) do |r| expectations.each do |expect| expect(r.stdout).to match(expect) end end end end # context 'with a different alias' context 'with a destkeypass' do command = if os[:family] == 'windows' interpolate_powershell("rm -force #{@temp_dir}pkcs12.ks") else "rm -f #{@temp_dir}pkcs12.ks" end before(:all) { run_shell(command, expect_failures: true) } it 'creates a private key with chain' do pp = <<-MANIFEST java_ks { 'Leaf_Cert:#{@temp_dir}/pkcs12.ks': ensure => #{@ensure_ks}, certificate => "#{@temp_dir}leaf.p12", destkeypass => "abcdef123456", storetype => 'pkcs12', password => 'puppet', path => #{@resource_path}, source_password => 'pkcs12pass', source_alias => 'Leaf Cert' } MANIFEST idempotent_apply(pp) end expectations = [ %r{Alias name: leaf_cert}, %r{Entry type: (keyEntry|PrivateKeyEntry)}, %r{Certificate chain length: 3}, %r{^Serial number: 5.*^Serial number: 4.*^Serial number: 3}m, ] it 'verifies the private key and chain' do run_shell(keytool_command("-list -v -keystore #{@temp_dir}pkcs12.ks -storepass puppet"), expect_failures: true) do |r| expectations.each do |expect| expect(r.stdout).to match(expect) end end end end # context 'with a destkeypass' end diff --git a/spec/acceptance/private_key_spec.rb b/spec/acceptance/private_key_spec.rb index 4d3a760..7b7365c 100644 --- a/spec/acceptance/private_key_spec.rb +++ b/spec/acceptance/private_key_spec.rb @@ -1,33 +1,35 @@ +# frozen_string_literal: true + require 'spec_helper_acceptance' describe 'managing java private keys' do # rubocop:disable RSpec/InstanceVariable : Instance variables are inherited and thus cannot be contained within lets include_context 'common variables' it 'creates a private key' do pp = <<-MANIFEST java_ks { 'broker.example.com:#{@temp_dir}private_key.ts': ensure => #{@ensure_ks}, certificate => "#{@temp_dir}ca.pem", private_key => "#{@temp_dir}privkey.pem", password => 'puppet', path => #{@resource_path}, } MANIFEST idempotent_apply(pp) end expectations = [ %r{Alias name: broker\.example\.com}, %r{Entry type: (keyEntry|PrivateKeyEntry)}, %r{CN=Test CA}, ] it 'verifies the private key' do run_shell(keytool_command("-list -v -keystore #{@temp_dir}private_key.ts -storepass puppet"), expect_failures: true) do |r| expectations.each do |expect| expect(r.stdout).to match(expect) end end end end diff --git a/spec/acceptance/truststore_spec.rb b/spec/acceptance/truststore_spec.rb index 0490b54..506f81f 100644 --- a/spec/acceptance/truststore_spec.rb +++ b/spec/acceptance/truststore_spec.rb @@ -1,61 +1,63 @@ +# frozen_string_literal: true + require 'spec_helper_acceptance' describe 'managing java truststores' do # rubocop:disable RSpec/InstanceVariable : Instance variables are inherited and thus cannot be contained within lets include_context 'common variables' it 'creates a truststore' do command = "rm #{@temp_dir}truststore.ts" command = interpolate_powershell(command) if os[:family] == 'windows' run_shell(command, expect_failures: true) pp = <<-EOS java_ks { 'puppetca:#{@temp_dir}truststore': ensure => #{@ensure_ks}, certificate => "#{@temp_dir}ca.pem", target => "#{@temp_dir}truststore.ts", password => 'puppet', trustcacerts => true, path => #{@resource_path}, } EOS idempotent_apply(pp) end expectations = [ %r{Your keystore contains 1 entry}, %r{Alias name: puppetca}, %r{CN=Test CA}, ] it 'verifies the truststore' do run_shell(keytool_command("-list -v -keystore #{@temp_dir}truststore.ts -storepass puppet")) do |r| expect(r.exit_code).to be_zero expectations.each do |expect| expect(r.stdout).to match(expect) end end end it 'recreates a truststore if password fails' do pp = <<-MANIFEST java_ks { 'puppetca:#{@temp_dir}truststore': ensure => latest, certificate => "#{@temp_dir}ca.pem", target => "#{@temp_dir}truststore.ts", password => 'bobinsky', password_fail_reset => true, trustcacerts => true, path => #{@resource_path}, } MANIFEST idempotent_apply(pp) end it 'verifies the truststore again' do run_shell(keytool_command("-list -v -keystore #{@temp_dir}truststore.ts -storepass bobinsky")) do |r| expect(r.exit_code).to be_zero expectations.each do |expect| expect(r.stdout).to match(expect) end end end end diff --git a/spec/spec_helper_acceptance_local.rb b/spec/spec_helper_acceptance_local.rb index 72da642..29e77fc 100644 --- a/spec/spec_helper_acceptance_local.rb +++ b/spec/spec_helper_acceptance_local.rb @@ -1,197 +1,199 @@ +# frozen_string_literal: true + UNSUPPORTED_PLATFORMS = [].freeze require 'singleton' class LitmusHelper include Singleton include PuppetLitmus end def keytool_command(arguments) # The @keytool global does not exist right now as the function is defined. # When the tests call the function, RSpec.shared_context below will have run # by then and the variable will exist. # os[:family] == 'windows' ? interpolate_powershell("& '#{@keytool_path}keytool'") : "'#{@keytool_path}keytool'" if os[:family] == 'windows' interpolate_powershell("& '#{@keytool_path}keytool' #{arguments}") else "'#{@keytool_path}keytool' #{arguments}" end end def interpolate_powershell(command) "powershell.exe -NoProfile -Nologo -Command \"#{command}\"" end def remote_windows_temp_dir @remote_windows_temp_dir ||= LitmusHelper.instance.run_shell(interpolate_powershell('echo "$ENV:TEMP"')).stdout.strip.tr('\\', '/') + '/' @remote_windows_temp_dir end def remote_file_exists?(filename) if os[:family] == 'windows' LitmusHelper.instance.run_shell(interpolate_powershell("Get-Item -Path '#{filename}' -ErrorAction SilentlyContinue"), expect_failures: true) else LitmusHelper.instance.run_shell("test -f '#{filename}'", expect_failures: true) end end def temp_dir @temp_dir ||= (os[:family] == 'windows') ? remote_windows_temp_dir : '/tmp/' @temp_dir end def create_and_upload_certs cert_files = ['privkey.pem', 'ca.pem', 'ca.der', 'ca2.pem', 'chain.pem', 'chain2.pem', 'leafkey.pem', 'leaf.pem', 'leafchain.pem', 'leafchain2.pem', 'leaf.p12', 'leaf2.p12'] recreate_certs = false cert_files.each do |cert_file| recreate_certs = true unless File.file?("spec/acceptance/certs/#{cert_file}") end create_certs if recreate_certs cert_files.each do |cert_file| if ENV['TARGET_HOST'].nil? || ENV['TARGET_HOST'] == 'localhost' command = "cp spec\\acceptance\\certs\\#{cert_file} #{ENV['TEMP']}\\#{cert_file}" command = interpolate_powershell(command) if os[:family] == 'windows' Open3.capture3(command) else LitmusHelper.instance.bolt_upload_file("spec/acceptance/certs/#{cert_file}", "#{temp_dir}#{cert_file}") end end end def create_certs require 'openssl' key = OpenSSL::PKey::RSA.new 1024 ca = OpenSSL::X509::Certificate.new ca.serial = 1 ca.public_key = key.public_key subj = '/CN=Test CA/ST=Denial/L=Springfield/O=Dis/CN=www.example.com' ca.subject = OpenSSL::X509::Name.parse subj ca.issuer = ca.subject ca.not_before = Time.now ca.not_after = ca.not_before + 360 ca.sign(key, OpenSSL::Digest::SHA256.new) key2 = OpenSSL::PKey::RSA.new 1024 ca2 = OpenSSL::X509::Certificate.new ca2.serial = 2 ca2.public_key = key2.public_key subj2 = '/CN=Test CA/ST=Denial/L=Springfield/O=Dis/CN=www.example.com' ca2.subject = OpenSSL::X509::Name.parse subj2 ca2.issuer = ca2.subject ca2.not_before = Time.now ca2.not_after = ca2.not_before + 360 ca2.sign(key2, OpenSSL::Digest::SHA256.new) key_chain = OpenSSL::PKey::RSA.new 1024 chain = OpenSSL::X509::Certificate.new chain.serial = 3 chain.public_key = key_chain.public_key chain_subj = '/CN=Chain CA/ST=Denial/L=Springfield/O=Dis/CN=www.example.net' chain.subject = OpenSSL::X509::Name.parse chain_subj chain.issuer = ca.subject chain.not_before = Time.now chain.not_after = chain.not_before + 360 chain.sign(key, OpenSSL::Digest::SHA256.new) key_chain2 = OpenSSL::PKey::RSA.new 1024 chain2 = OpenSSL::X509::Certificate.new chain2.serial = 4 chain2.public_key = key_chain2.public_key chain2_subj = '/CN=Chain CA 2/ST=Denial/L=Springfield/O=Dis/CN=www.example.net' chain2.subject = OpenSSL::X509::Name.parse chain2_subj chain2.issuer = chain.subject chain2.not_before = Time.now chain2.not_after = chain2.not_before + 360 chain2.sign(key_chain, OpenSSL::Digest::SHA256.new) key_leaf = OpenSSL::PKey::RSA.new 1024 leaf = OpenSSL::X509::Certificate.new leaf.serial = 5 leaf.public_key = key_leaf.public_key leaf_subj = '/CN=Leaf Cert/ST=Denial/L=Springfield/O=Dis/CN=www.example.net' leaf.subject = OpenSSL::X509::Name.parse leaf_subj leaf.issuer = chain2.subject leaf.not_before = Time.now leaf.not_after = leaf.not_before + 360 leaf.sign(key_chain2, OpenSSL::Digest::SHA256.new) chain3 = OpenSSL::X509::Certificate.new chain3.serial = 6 chain3.public_key = key_chain2.public_key chain3.subject = OpenSSL::X509::Name.parse chain2_subj chain3.issuer = ca.subject chain3.not_before = Time.now chain3.not_after = chain3.not_before + 360 chain3.sign(key, OpenSSL::Digest::SHA256.new) pkcs12 = OpenSSL::PKCS12.create('pkcs12pass', 'Leaf Cert', key_leaf, leaf, [chain2, chain]) pkcs12_chain3 = OpenSSL::PKCS12.create('pkcs12pass', 'Leaf Cert', key_leaf, leaf, [chain3]) create_cert_file('privkey.pem', key.to_pem) create_cert_file('ca.pem', ca.to_pem) create_cert_file('ca.der', ca.to_der) create_cert_file('ca2.pem', ca2.to_pem) create_cert_file('chain.pem', chain2.to_pem + chain.to_pem) create_cert_file('chain2.pem', chain3.to_pem) create_cert_file('leafkey.pem', key_leaf.to_pem) create_cert_file('leaf.pem', leaf.to_pem) create_cert_file('leafchain.pem', leaf.to_pem + chain2.to_pem + chain.to_pem) create_cert_file('leafchain2.pem', leaf.to_pem + chain3.to_pem) create_cert_file('leaf.p12', pkcs12.to_der) create_cert_file('leaf2.p12', pkcs12_chain3.to_der) end def create_cert_file(cert_name, contents) return if File.file?("spec/acceptance/certs/#{cert_name}") out_file = File.new("spec/acceptance/certs/#{cert_name}", 'w+') out_file.puts(contents) out_file.close end RSpec.configure do |c| c.before :suite do create_and_upload_certs # install java if windows if os[:family] == 'windows' LitmusHelper.instance.run_shell('puppet module install puppetlabs-chocolatey') pp_one = <<-MANIFEST include chocolatey package { 'jdk8': ensure => '8.0.211', provider => 'chocolatey' } MANIFEST LitmusHelper.instance.apply_manifest(pp_one) else LitmusHelper.instance.run_shell('puppet module install puppetlabs-java') pp_two = <<-MANIFEST class { 'java': } MANIFEST LitmusHelper.instance.apply_manifest(pp_two) end end end RSpec.shared_context 'common variables' do before(:each) do java_major, java_minor = (ENV['JAVA_VERSION'] || '8u211').split('u') @ensure_ks = 'latest' @resource_path = 'undef' @target_dir = '/etc/' @temp_dir = temp_dir case os[:family] when 'solaris' @keytool_path = '/usr/java/bin/' @resource_path = "['/usr/java/bin/','/opt/puppet/bin/']" when 'aix' @keytool_path = '/usr/java6/bin/' @resource_path = "['/usr/java6/bin/','/usr/bin/']" when 'windows' @ensure_ks = 'present' @keytool_path = "C:/Program Files/Java/jdk1.#{java_major}.0_#{java_minor}/bin/" @resource_path = "['C:/Program Files/Java/jdk1.#{java_major}.0_#{java_minor}/bin/']" when 'ubuntu' @ensure_ks = 'present' if os[:release] == '20.04' end end end diff --git a/spec/spec_helper_local.rb b/spec/spec_helper_local.rb index 2e897cb..ce4d062 100644 --- a/spec/spec_helper_local.rb +++ b/spec/spec_helper_local.rb @@ -1,28 +1,30 @@ +# frozen_string_literal: true + if ENV['COVERAGE'] == 'yes' require 'simplecov' require 'simplecov-console' require 'codecov' SimpleCov.formatters = [ SimpleCov::Formatter::HTMLFormatter, SimpleCov::Formatter::Console, SimpleCov::Formatter::Codecov, ] SimpleCov.start do track_files 'lib/**/*.rb' add_filter '/spec' # do not track vendored files add_filter '/vendor' add_filter '/.vendor' # do not track gitignored files # this adds about 4 seconds to the coverage check # this could definitely be optimized add_filter do |f| # system returns true if exit status is 0, which with git-check-ignore means file is ignored system("git check-ignore --quiet #{f.filename}") end end end diff --git a/spec/unit/puppet/provider/java_ks/keytool_spec.rb b/spec/unit/puppet/provider/java_ks/keytool_spec.rb index f200d28..642a4df 100644 --- a/spec/unit/puppet/provider/java_ks/keytool_spec.rb +++ b/spec/unit/puppet/provider/java_ks/keytool_spec.rb @@ -1,221 +1,223 @@ #!/usr/bin/env rspec +# frozen_string_literal: true + require 'spec_helper' describe Puppet::Type.type(:java_ks).provider(:keytool) do let(:temp_dir) do if Puppet.features.microsoft_windows? ENV['TEMP'] else '/tmp/' end end let(:global_params) do { title: "app.example.com:#{temp_dir}application.jks", name: 'app.example.com', target: "#{temp_dir}application.jks", password: 'puppet', certificate: "#{temp_dir}app.example.com.pem", private_key: "#{temp_dir}private/app.example.com.pem", storetype: 'jceks', provider: described_class.name, } end let(:params) do global_params end let(:resource) do Puppet::Type.type(:java_ks).new(params) end let(:provider) do resource.provider end before(:each) do allow(provider).to receive(:command).with(:keytool).and_return('mykeytool') allow(provider).to receive(:command).with(:openssl).and_return('myopenssl') allow(provider).to receive(:command_keytool).and_return('mykeytool') allow(provider).to receive(:command_openssl).and_return('myopenssl') tempfile = class_double('tempfile', class: Tempfile, write: true, flush: true, close!: true, path: "#{temp_dir}testing.stuff") allow(Tempfile).to receive(:new).and_return(tempfile) end describe 'when updating a certificate' do it 'calls destroy and create' do expect(provider).to receive(:destroy) expect(provider).to receive(:create) provider.update end end describe 'when running keystore commands', if: !Puppet.features.microsoft_windows? do it 'calls the passed command' do cmd = '/bin/echo testing 1 2 3' exec_class = if Puppet::Util::Execution.respond_to?(:execute) Puppet::Util::Execution else Puppet::Util end expect(exec_class).to receive(:execute).with( cmd, failonfail: true, combine: true, ) provider.run_command(cmd) end context 'short timeout' do let(:params) do global_params.merge(keytool_timeout: 0.1) end it 'errors if timeout occurs' do cmd = 'sleep 1' expect { provider.run_command(cmd) }.to raise_error Puppet::Error, "Timed out waiting for 'app.example.com' to run keytool" end end it 'normally times out after 120 seconds' do cmd = '/bin/echo testing 1 2 3' expect(Timeout).to receive(:timeout).with(120, Timeout::Error).and_raise(Timeout::Error) expect { provider.run_command(cmd) }.to raise_error Puppet::Error, "Timed out waiting for 'app.example.com' to run keytool" end end describe 'when importing a private key and certifcate' do describe '#to_pkcs12' do it 'converts a certificate to a pkcs12 file' do sleep 0.1 # due to https://github.com/mitchellh/vagrant/issues/5056 testing_key = OpenSSL::PKey::RSA.new 1024 testing_ca = OpenSSL::X509::Certificate.new testing_ca.serial = 1 testing_ca.public_key = testing_key.public_key testing_subj = '/CN=Test CA/ST=Denial/L=Springfield/O=Dis/CN=www.example.com' testing_ca.subject = OpenSSL::X509::Name.parse testing_subj testing_ca.issuer = testing_ca.subject testing_ca.not_before = Time.now testing_ca.not_after = testing_ca.not_before + 360 testing_ca.sign(testing_key, OpenSSL::Digest::SHA256.new) allow(provider).to receive(:password).and_return(resource[:password]) allow(File).to receive(:read).with(resource[:private_key]).and_return('private key') allow(File).to receive(:read).with(resource[:certificate], hash_including(encoding: 'ISO-8859-1')).and_return(testing_ca.to_pem) expect(OpenSSL::PKey::RSA).to receive(:new).with('private key', 'puppet').and_return('priv_obj') expect(OpenSSL::X509::Certificate).to receive(:new).with(testing_ca.to_pem.chomp).and_return('cert_obj') pkcs_double = BogusPkcs.new expect(pkcs_double).to receive(:to_der) expect(OpenSSL::PKCS12).to receive(:create).with(resource[:password], resource[:name], 'priv_obj', 'cert_obj', []).and_return(pkcs_double) provider.to_pkcs12("#{temp_dir}testing.stuff") end end describe '#import_ks' do it 'executes openssl and keytool with specific options' do expect(provider).to receive(:to_pkcs12).with("#{temp_dir}testing.stuff") expect(provider).to receive(:run_command).with(['mykeytool', '-importkeystore', '-srcstoretype', 'PKCS12', '-destkeystore', resource[:target], '-srckeystore', "#{temp_dir}testing.stuff", '-alias', resource[:name]], any_args) provider.import_ks end it 'uses destkeypass when provided' do dkp = resource.dup dkp[:destkeypass] = 'keypass' expect(provider).to receive(:to_pkcs12).with("#{temp_dir}testing.stuff") expect(provider).to receive(:run_command).with(['mykeytool', '-importkeystore', '-srcstoretype', 'PKCS12', '-destkeystore', dkp[:target], '-srckeystore', "#{temp_dir}testing.stuff", '-alias', dkp[:name], '-destkeypass', dkp[:destkeypass]], any_args) provider.import_ks end end end describe 'when importing a pkcs12 file' do let(:params) do { title: "app.example.com:#{temp_dir}testing.jks", name: 'app.example.com', target: "#{temp_dir}application.jks", password: 'puppet', certificate: "#{temp_dir}testing.p12", storetype: 'pkcs12', source_password: 'password', provider: described_class.name, } end let(:resource) do Puppet::Type.type(:java_ks).new(params) end let(:provider) do resource.provider end describe '#import_pkcs12' do it 'supports pkcs12 source' do pkcs12 = resource.dup pkcs12[:storetype] = 'pkcs12' expect(provider).to receive(:run_command).with(['mykeytool', '-importkeystore', '-srcstoretype', 'PKCS12', '-destkeystore', pkcs12[:target], '-srckeystore', "#{temp_dir}testing.p12"], any_args) provider.import_pkcs12 end end end describe 'when creating entries in a keystore' do let(:params) do { title: "app.example.com:#{temp_dir}application.jks", name: 'app.example.com', target: "#{temp_dir}application.jks", password: 'puppet', certificate: "#{temp_dir}app.example.com.pem", private_key: "#{temp_dir}private/app.example.com.pem", provider: described_class.name, } end let(:resource) do Puppet::Type.type(:java_ks).new(params) end let(:provider) do resource.provider end it 'calls import_ks if private_key and certificate are provided' do expect(provider).to receive(:import_ks) provider.create end it 'calls keytool with specific options if only certificate is provided' do no_pk = resource.dup no_pk.delete(:private_key) expect(provider).to receive(:run_command).with(['mykeytool', '-importcert', '-noprompt', '-alias', no_pk[:name], '-file', no_pk[:certificate], '-keystore', no_pk[:target]], any_args) expect(no_pk.provider).to receive(:import_ks).never no_pk.provider.create end end describe 'when removing entries from keytool' do it 'executes keytool with a specific set of options' do expect(provider).to receive(:run_command).with(['mykeytool', '-delete', '-alias', resource[:name], '-keystore', resource[:target], '-storetype', resource[:storetype]], any_args) provider.destroy end end end class BogusPkcs end diff --git a/spec/unit/puppet/type/java_ks_spec.rb b/spec/unit/puppet/type/java_ks_spec.rb index 0a78491..7b0d222 100644 --- a/spec/unit/puppet/type/java_ks_spec.rb +++ b/spec/unit/puppet/type/java_ks_spec.rb @@ -1,243 +1,245 @@ +# frozen_string_literal: true + ! # /usr/bin/env rspec require 'spec_helper' describe Puppet::Type.type(:java_ks) do let(:temp_dir) do if Puppet.features.microsoft_windows? ENV['TEMP'] else '/tmp/' end end let(:provider_var) { class_double('provider', class: described_class.defaultprovider, clear: nil) } let(:app_example_com) do { title: "app.example.com:#{temp_dir}application.jks", name: 'app.example.com', target: "#{temp_dir}application.jks", password: 'puppet', destkeypass: 'keypass', certificate: "#{temp_dir}app.example.com.pem", private_key: "#{temp_dir}private/app.example.com.pem", private_key_type: 'rsa', storetype: 'jceks', provider: :keytool, } end let(:jks_resource) do app_example_com end before(:each) do allow(described_class.defaultprovider).to receive(:new).and_return(provider_var) end it 'defaults to being present' do expect(described_class.new(app_example_com)[:ensure]).to eq(:present) end describe 'when validating attributes' do [:name, :target, :private_key, :private_key_type, :certificate, :password, :password_file, :trustcacerts, :destkeypass, :password_fail_reset, :source_password].each do |param| it "should have a #{param} parameter" do expect(described_class.attrtype(param)).to eq(:param) end end [:ensure].each do |prop| it "should have a #{prop} property" do expect(described_class.attrtype(prop)).to eq(:property) end end end describe 'when validating attribute values' do [:present, :absent, :latest].each do |value| it "should support #{value} as a value to ensure" do described_class.new(jks_resource.merge(ensure: value)) end end it 'first half of title should map to name parameter' do jks = jks_resource.dup jks.delete(:name) expect(described_class.new(jks)[:name]).to eq(jks_resource[:name]) end it 'second half of title should map to target parameter when no target is supplied' do jks = jks_resource.dup jks.delete(:target) expect(described_class.new(jks)[:target]).to eq(jks_resource[:target]) end it 'second half of title should not map to target parameter when target is supplied #not to equal' do jks = jks_resource.dup jks[:target] = "#{temp_dir}some_other_app.jks" expect(described_class.new(jks)[:target]).not_to eq(jks_resource[:target]) end it 'second half of title should not map to target parameter when target is supplied #to equal' do jks = jks_resource.dup jks[:target] = "#{temp_dir}some_other_app.jks" expect(described_class.new(jks)[:target]).to eq("#{temp_dir}some_other_app.jks") end it 'title components should map to namevar parameters #name' do jks = jks_resource.dup jks.delete(:name) jks.delete(:target) expect(described_class.new(jks)[:name]).to eq(jks_resource[:name]) end it 'title components should map to namevar parameters #target' do jks = jks_resource.dup jks.delete(:name) jks.delete(:target) expect(described_class.new(jks)[:target]).to eq(jks_resource[:target]) end it 'downcases :name values' do jks = jks_resource.dup jks[:name] = 'APP.EXAMPLE.COM' expect(described_class.new(jks)[:name]).to eq(jks_resource[:name]) end it 'has :false value to :trustcacerts when parameter not provided' do expect(described_class.new(jks_resource)[:trustcacerts]).to eq(:false) end it 'has :rsa as the default value for :private_key_type' do expect(described_class.new(jks_resource)[:private_key_type]).to eq(:rsa) end it 'fails if :private_key_type is neither :rsa nor :ec nor :dsa' do jks = jks_resource.dup jks[:private_key_type] = 'nosuchkeytype' expect { described_class.new(jks) }.to raise_error(Puppet::Error) end it 'fails if both :password and :password_file are provided' do jks = jks_resource.dup jks[:password_file] = '/path/to/password_file' expect { described_class.new(jks) }.to raise_error(Puppet::Error, %r{You must pass either}) end it 'fails if neither :password or :password_file is provided' do jks = jks_resource.dup jks.delete(:password) expect { described_class.new(jks) }.to raise_error(Puppet::Error, %r{You must pass one of}) end it 'fails if :password is fewer than 6 characters' do jks = jks_resource.dup jks[:password] = 'aoeui' expect { described_class.new(jks) }.to raise_error(Puppet::Error, %r{6 characters}) end it 'fails if :destkeypass is fewer than 6 characters' do jks = jks_resource.dup jks[:destkeypass] = 'aoeui' expect { described_class.new(jks) }.to raise_error(Puppet::Error, %r{length 6}) end it 'has :false value to :password_fail_reset when parameter not provided' do expect(described_class.new(jks_resource)[:password_fail_reset]).to eq(:false) end it 'fails if :source_password is not provided for pkcs12 :storetype' do jks = jks_resource.dup jks[:storetype] = 'pkcs12' expect { described_class.new(jks) }.to raise_error(Puppet::Error, %r{You must provide 'source_password' when using a 'pkcs12' storetype}) end end describe 'when ensure is set to latest' do it 'insync? should return false if sha1 fingerprints do not match and state is :present' do jks = jks_resource.dup jks[:ensure] = :latest allow(provider_var).to receive(:latest).and_return('9B:8B:23:4C:6A:9A:08:F6:4E:B6:01:23:EA:5A:E7:8F:6A') allow(provider_var).to receive(:current).and_return('21:46:45:65:57:50:FE:2D:DA:7C:C8:57:D2:33:3A:B0:A6') expect(described_class.new(jks).property(:ensure)).not_to be_insync(:present) end it 'insync? should return false if state is :absent' do jks = jks_resource.dup jks[:ensure] = :latest expect(described_class.new(jks).property(:ensure)).not_to be_insync(:absent) end it 'insync? should return true if sha1 fingerprints match and state is :present' do jks = jks_resource.dup jks[:ensure] = :latest allow(provider_var).to receive(:latest).and_return('66:9B:8B:23:4C:6A:9A:08:F6:4E:B6:01:23:EA:5A') allow(provider_var).to receive(:current).and_return('66:9B:8B:23:4C:6A:9A:08:F6:4E:B6:01:23:EA:5A') expect(described_class.new(jks).property(:ensure)).to be_insync(:present) end end describe 'when file resources are in the catalog' do let(:file_provider) { class_double('provider', class: Puppet::Type.type(:file).defaultprovider, clear: nil) } before(:each) do allow(Puppet::Type.type(:file).defaultprovider).to receive(:new).and_return(file_provider) end [:private_key, :certificate].each do |file| it "should autorequire for #{file} #file" do test_jks = described_class.new(jks_resource) test_file = Puppet::Type.type(:file).new(title: jks_resource[file]) Puppet::Resource::Catalog.new :testing do |conf| [test_jks, test_file].each { |resource| conf.add_resource resource } end rel = test_jks.autorequire[0] expect(rel.source.ref).to eq(test_file.ref) end it "should autorequire for #{file} #jks" do test_jks = described_class.new(jks_resource) test_file = Puppet::Type.type(:file).new(title: jks_resource[file]) Puppet::Resource::Catalog.new :testing do |conf| [test_jks, test_file].each { |resource| conf.add_resource resource } end rel = test_jks.autorequire[0] expect(rel.target.ref).to eq(test_jks.ref) end end it 'autorequires for the :target directory #file' do test_jks = described_class.new(jks_resource) test_file = Puppet::Type.type(:file).new(title: ::File.dirname(jks_resource[:target])) Puppet::Resource::Catalog.new :testing do |conf| [test_jks, test_file].each { |resource| conf.add_resource resource } end rel = test_jks.autorequire[0] expect(rel.source.ref).to eq(test_file.ref) end it 'autorequires for the :target directory #jks' do test_jks = described_class.new(jks_resource) test_file = Puppet::Type.type(:file).new(title: ::File.dirname(jks_resource[:target])) Puppet::Resource::Catalog.new :testing do |conf| [test_jks, test_file].each { |resource| conf.add_resource resource } end rel = test_jks.autorequire[0] expect(rel.target.ref).to eq(test_jks.ref) end end end