summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJohn McCrae <john.mccrae@progress.com>2022-01-04 12:52:02 -0800
committerJohn McCrae <john.mccrae@progress.com>2022-01-18 13:32:41 -0800
commitaf0257777956496428f84d73eeb36f80dec057bc (patch)
treef49e950b8bb047896e8fc429da1cdc08f1a8983d
parentd6e9330b34ab6041a5bc5abc7ff67e09433431a4 (diff)
downloadchef-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.lock6
-rw-r--r--chef-universal-mingw32.gemspec1
-rw-r--r--lib/chef/client.rb8
-rw-r--r--lib/chef/http/authenticator.rb108
-rw-r--r--lib/chef/mixin/powershell_exec.rb32
-rw-r--r--lib/chef/win32/registry.rb6
-rw-r--r--spec/integration/client/client_spec.rb72
-rw-r--r--spec/unit/client_spec.rb4
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.