diff options
author | John McCrae <john.mccrae@progress.com> | 2022-01-04 12:52:02 -0800 |
---|---|---|
committer | John McCrae <john.mccrae@progress.com> | 2022-01-18 13:32:41 -0800 |
commit | af0257777956496428f84d73eeb36f80dec057bc (patch) | |
tree | f49e950b8bb047896e8fc429da1cdc08f1a8983d | |
parent | d6e9330b34ab6041a5bc5abc7ff67e09433431a4 (diff) | |
download | chef-af0257777956496428f84d73eeb36f80dec057bc.tar.gz |
Updated the chef client to retrieve certs from the Windows registry. Tests included. This is PR3 since I keep trashing them
Signed-off-by: John McCrae <john.mccrae@progress.com>
-rw-r--r-- | Gemfile.lock | 6 | ||||
-rw-r--r-- | chef-universal-mingw32.gemspec | 1 | ||||
-rw-r--r-- | lib/chef/client.rb | 8 | ||||
-rw-r--r-- | lib/chef/http/authenticator.rb | 108 | ||||
-rw-r--r-- | lib/chef/mixin/powershell_exec.rb | 32 | ||||
-rw-r--r-- | lib/chef/win32/registry.rb | 6 | ||||
-rw-r--r-- | spec/integration/client/client_spec.rb | 72 | ||||
-rw-r--r-- | spec/unit/client_spec.rb | 4 |
8 files changed, 199 insertions, 38 deletions
diff --git a/Gemfile.lock b/Gemfile.lock index 566457e935..3347843e45 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -164,6 +164,9 @@ GEM debug_inspector (>= 0.0.1) builder (3.2.4) byebug (11.1.3) + chef-powershell (1.0.6) + ffi (~> 1.15) + ffi-yajl (~> 2.4) chef-telemetry (1.1.1) chef-config concurrent-ruby (~> 1.0) @@ -438,7 +441,6 @@ GEM wmi-lite (1.0.5) PLATFORMS - arm64-darwin-20 ruby x64-mingw32 x86-mingw32 @@ -466,4 +468,4 @@ DEPENDENCIES webmock BUNDLED WITH - 2.2.32 + 2.2.29 diff --git a/chef-universal-mingw32.gemspec b/chef-universal-mingw32.gemspec index 329b4f9d6b..314b40effb 100644 --- a/chef-universal-mingw32.gemspec +++ b/chef-universal-mingw32.gemspec @@ -15,6 +15,7 @@ gemspec.add_dependency "wmi-lite", "~> 1.0" gemspec.add_dependency "win32-taskscheduler", "~> 2.0" gemspec.add_dependency "iso8601", ">= 0.12.1", "< 0.14" # validate 0.14 when it comes out gemspec.add_dependency "win32-certstore", "~> 0.6.2" # 0.5+ required for specifying user vs. system store +gemspec.add_dependency "chef-powershell", "~> 1.0.6" gemspec.extensions << "ext/win32-eventlog/Rakefile" gemspec.files += Dir.glob("{distro,ext}/**/*") diff --git a/lib/chef/client.rb b/lib/chef/client.rb index 7f184d7db4..006591ce1c 100644 --- a/lib/chef/client.rb +++ b/lib/chef/client.rb @@ -229,7 +229,7 @@ class Chef start_profiling runlock = RunLock.new(Chef::Config.lockfile) - # TODO feels like acquire should have its own block arg for this + # TODO: feels like acquire should have its own block arg for this runlock.acquire # don't add code that may fail before entering this section to be sure to release lock begin @@ -637,7 +637,11 @@ class Chef # @api private # def register(client_name = node_name, config = Chef::Config) - if !config[:client_key] + if Chef::HTTP::Authenticator.detect_certificate_key(client_name) + events.skipping_registration(client_name, config) + logger.trace("Client key #{client_name} is present in certificate repository - skipping registration") + config[:client_key] = "Cert:\\LocalMachine\\My\\chef-#{client_name}" + elsif !config[:client_key] events.skipping_registration(client_name, config) logger.trace("Client key is unspecified - skipping registration") elsif File.exists?(config[:client_key]) diff --git a/lib/chef/http/authenticator.rb b/lib/chef/http/authenticator.rb index 80b32be750..02d6a2ead8 100644 --- a/lib/chef/http/authenticator.rb +++ b/lib/chef/http/authenticator.rb @@ -16,16 +16,19 @@ # limitations under the License. # +require "chef/mixin/powershell_exec" require_relative "auth_credentials" require_relative "../exceptions" +require_relative "../win32/registry" autoload :OpenSSL, "openssl" class Chef class HTTP class Authenticator - DEFAULT_SERVER_API_VERSION = "2".freeze + extend Chef::Mixin::PowershellExec + attr_reader :signing_key_filename attr_reader :raw_key attr_reader :attr_names @@ -83,8 +86,53 @@ class Chef @auth_credentials.client_name end + def detect_certificate_key(client_name) + self.class.detect_certificate_key(client_name) + end + + def check_certstore_for_key(client_name) + self.class.check_certstore_for_key(client_name) + end + + def retrieve_certificate_key(client_name) + self.class.retrieve_certificate_key(client_name) + end + + # Detects if a private key exists in a certificate repository like Keychain (macOS) or Certificate Store (Windows) + # + # @param client_name - we're using the node name to store and retrieve any keys + # Returns true if a key is found, false if not. False will trigger a registration event which will lead to a certificate based key being created + # + def self.detect_certificate_key(client_name) + if ChefUtils.windows? + check_certstore_for_key(client_name) + else # generic return for Mac and LInux clients + false + end + end + + def self.check_certstore_for_key(client_name) + powershell_code = <<~CODE + $cert = Get-ChildItem -path cert:\\LocalMachine\\My -Recurse -Force | Where-Object { $_.Subject -Match "#{client_name}" } -ErrorAction Stop + if (($cert.HasPrivateKey -eq $true) -and ($cert.PrivateKey.Key.ExportPolicy -ne "NonExportable")) { + return $true + } + else{ + return $false + } + CODE + powershell_exec!(powershell_code).result + end + def load_signing_key(key_file, raw_key = nil) - if !!key_file + results = retrieve_certificate_key(Chef::Config[:node_name]) + + if key_file == nil? && raw_key == nil? + puts "\nNo key detected\n" + elsif results != false + # results variable can be 1 of 2 values - "False" or the contents of a key. + @raw_key = results + elsif !!key_file @raw_key = IO.read(key_file).strip elsif !!raw_key @raw_key = raw_key.strip @@ -104,6 +152,62 @@ class Chef raise Chef::Exceptions::InvalidPrivateKey, msg end + # takes no parameters. Checks for the password in the registry and returns it if there, otherwise returns false + def self.get_cert_password + @win32registry = Chef::Win32::Registry.new + path = "HKEY_LOCAL_MACHINE\\Software\\Progress\\Authentication" + present = @win32registry.get_values(path) + if present.map { |h| h[:name] }[0] == "PfxPass" + present.map { |h| h[:data] }[0].to_s + end + rescue Chef::Exceptions::Win32RegKeyMissing + # if we don't have a password, log that and generate one + Chef::Log.warn "Authentication Hive and value not present in registry, creating it now" + new_path = "HKEY_LOCAL_MACHINE\\Software\\Progress\\Authentication" + # cspell:disable-next-line + some_chars = "~!@#$%^&*_-+=`|\\(){}[<]:;'>,.?/0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz".each_char.to_a + password = some_chars.sample(1 + rand(some_chars.count)).join[0...14] + values = { name: "PfxPass", type: :string, data: password } + @win32registry.set_value(new_path, values) + password + end + + def self.retrieve_certificate_key(client_name) + require "openssl" unless defined?(OpenSSL) + + if ChefUtils.windows? + + password = get_cert_password + + unless password + return false + end + + powershell_code = <<~CODE + Try { + $my_pwd = ConvertTo-SecureString -String "#{password}" -Force -AsPlainText; + $cert = Get-ChildItem -path cert:\\LocalMachine\\My -Recurse | Where-Object { $_.Subject -match "#{client_name}" } -ErrorAction Stop; + $tempfile = [System.IO.Path]::GetTempPath() + "export_pfx.pfx"; + Export-PfxCertificate -Cert $cert -Password $my_pwd -FilePath $tempfile; + } + Catch { + return $false + } + CODE + my_result = powershell_exec!(powershell_code).result + + if my_result != false + pkcs = OpenSSL::PKCS12.new(File.binread(my_result["PSPath"].split("::")[1]), password) + ::File.delete(my_result["PSPath"].split("::")[1]) + OpenSSL::PKey::RSA.new(pkcs.key.to_pem) + else + false + end + else # doing nothing for the Mac and Linux clients for now + false + end + end + def authentication_headers(method, url, json_body = nil, headers = nil) request_params = { http_method: method, diff --git a/lib/chef/mixin/powershell_exec.rb b/lib/chef/mixin/powershell_exec.rb index 75114220cb..0482d4d0e1 100644 --- a/lib/chef/mixin/powershell_exec.rb +++ b/lib/chef/mixin/powershell_exec.rb @@ -15,9 +15,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -require_relative "../powershell" -require_relative "../pwsh" - # The powershell_exec mixin provides in-process access to the PowerShell engine. # # powershell_exec is initialized with a string that should be set to the script @@ -95,35 +92,12 @@ require_relative "../pwsh" # credentials of the user running Chef Client are used. # +require "chef-powershell" + class Chef module Mixin module PowershellExec - # Run a command under PowerShell via a managed (.NET) API. - # - # Requires: .NET Framework 4.0 or higher on the target machine. - # - # @param script [String] script to run - # @param interpreter [Symbol] the interpreter type, `:powershell` or `:pwsh` - # @param timeout [Integer, nil] timeout in seconds. - # @return [Chef::PowerShell] output - def powershell_exec(script, interpreter = :powershell, timeout: -1) - case interpreter - when :powershell - Chef::PowerShell.new(script, timeout: timeout) - when :pwsh - Chef::Pwsh.new(script, timeout: timeout) - else - raise ArgumentError, "Expected interpreter of :powershell or :pwsh" - end - end - - # The same as the #powershell_exec method except this will raise - # Chef::PowerShell::CommandFailed if the command fails - def powershell_exec!(script, interpreter = :powershell, **options) - cmd = powershell_exec(script, interpreter, **options) - cmd.error! - cmd - end + include Chef_PowerShell::ChefPowerShell::PowerShellExec end end end diff --git a/lib/chef/win32/registry.rb b/lib/chef/win32/registry.rb index 4f7f2b2d52..ec7783815d 100644 --- a/lib/chef/win32/registry.rb +++ b/lib/chef/win32/registry.rb @@ -293,7 +293,11 @@ class Chef end def machine_architecture - node[:kernel][:machine].to_sym + if node.nil? + ::RbConfig::CONFIG["target_cpu"] == "x64" ? :x86_64 : :i386 + else + node[:kernel][:machine].to_sym + end end def assert_architecture! diff --git a/spec/integration/client/client_spec.rb b/spec/integration/client/client_spec.rb index af35eadb74..27a1d3c0d8 100644 --- a/spec/integration/client/client_spec.rb +++ b/spec/integration/client/client_spec.rb @@ -4,6 +4,7 @@ require "chef/mixin/shell_out" require "tiny_server" require "tmpdir" require "chef-utils/dist" +require "chef/mixin/powershell_exec" describe "chef-client" do @@ -31,8 +32,48 @@ describe "chef-client" do @server = @api = nil end + def install_certificate_in_store + if ChefUtils.windows? + powershell_exec!("New-SelfSignedCertificate -certstorelocation cert:\\localmachine\\my -Subject #{client_name} -FriendlyName #{client_name} -KeyExportPolicy Exportable") + end + end + + def create_registry_key + powershell_exec! <<~EOH + $pfx_password = New-Object -TypeName PSObject + $pfx_password | Add-Member -MemberType ScriptProperty -Name "Password" -Value { ("~!@#$%^&*_-+=`|\\(){}[<]:;'>,.?/0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz".tochararray() | Sort-Object { Get-Random })[0..14] -join '' } + if (-not (Test-Path HKLM:\\SOFTWARE\\Progress)){ + New-Item -Path "HKLM:\\SOFTWARE\\Progress\\Authentication" -Force + New-ItemProperty -path "HKLM:\\SOFTWARE\\Progress\\Authentication" -name "PfxPass" -value $pfx_password.Password -PropertyType String + } + EOH + end + + def remove_certificate_from_store + powershell_exec! <<~EOH + Get-ChildItem -path cert:\\LocalMachine\\My -Recurse -Force | Where-Object { $_.Subject -Match "#{client_name}" } -ErrorAction Stop | Remove-Item + EOH + end + + def remove_registry_key + powershell_exec!("Remove-Item -Path HKLM:\\SOFTWARE\\Progress -Recurse") + end + + def verify_export_password_exists + powershell_exec! <<~EOH + Try { + $response = Get-ItemPropertyValue -Path "HKLM:\\Software\\Progress\\Authentication" -Name "PfxPass" -ErrorAction Stop + if ($response) {return $true} + } + Catch { + return $false + } + EOH + end + include IntegrationSupport include Chef::Mixin::ShellOut + include Chef::Mixin::PowershellExec let(:chef_dir) { File.join(__dir__, "..", "..", "..") } @@ -47,6 +88,7 @@ describe "chef-client" do # cf. CHEF-4914 let(:chef_client) { "bundle exec #{ChefUtils::Dist::Infra::CLIENT} --minimal-ohai" } let(:chef_solo) { "bundle exec #{ChefUtils::Dist::Solo::EXEC} --legacy-mode --minimal-ohai" } + let(:client_name) { "chef-973334" } context "when validation.pem in current Directory" do let(:validation_path) { "" } @@ -146,6 +188,36 @@ describe "chef-client" do result.error! end + if ChefUtils.windows? + context "and the private key is in the Windows CertStore" do + before do + # install the p12/pfx and make sure the key and password are stored in the registry + install_certificate_in_store + create_registry_key + end + + after do + # remove the p12/pfx and remove the registry key + remove_certificate_from_store + remove_registry_key + end + + it "should verify that the cert is loaded in the LocalMachine\\My" do + expect(Chef::HTTP::Authenticator).to receive(:check_certstore_for_key).and_call_original + expect(Chef::HTTP::Authenticator.check_certstore_for_key(client_name)).to eq(true) + end + + it "should verify that the export password for the pfx is loaded in the Registry" do + expect(verify_export_password_exists.result).to eq(true) + end + + it "should verify that a private key is returned to me" do + expect(Chef::HTTP::Authenticator).to receive(:retrieve_certificate_key).and_call_original + expect(Chef::HTTP::Authenticator.retrieve_certificate_key(client_name)).not_to be_falsey + end + end + end + context "and a private key" do before do file "mykey.pem", <<~EOM diff --git a/spec/unit/client_spec.rb b/spec/unit/client_spec.rb index b6a321c8e8..1e835ae398 100644 --- a/spec/unit/client_spec.rb +++ b/spec/unit/client_spec.rb @@ -113,6 +113,7 @@ shared_context "a client run" do # --Client.register # Make sure Client#register thinks the client key doesn't # exist, so it tries to register and create one. + allow(Chef::HTTP::Authenticator).to receive(:detect_certificate_key).with(fqdn).and_return(false) allow(File).to receive(:exists?).and_call_original expect(File).to receive(:exists?) .with(Chef::Config[:client_key]) @@ -201,7 +202,6 @@ shared_context "a client run" do # Post conditions: check that node has been filled in correctly expect(client).to receive(:run_started) - stub_for_run end end @@ -262,7 +262,7 @@ end # requires platform and platform_version be defined shared_examples "a completed run" do - include_context "run completed" + include_context "run completed" # should receive run_completed_successfully it "runs ohai, sets up authentication, loads node state, synchronizes policy, converges" do # This is what we're testing. |