summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJohn McCrae <john.mccrae@progress.com>2022-03-29 11:22:49 -0700
committerGitHub <noreply@github.com>2022-03-29 11:22:49 -0700
commit67c26b0b94787b43e0b1e82ec994223ca5720044 (patch)
tree11663adab10a6a13cecc1af25dcb9d642c08ae36
parent38fd05fcde6ee345dbf31ee15cc469019bce3dca (diff)
parent5e82b4b62c40cde2f4eb2774efe08fb211d4520d (diff)
downloadchef-67c26b0b94787b43e0b1e82ec994223ca5720044.tar.gz
Merge pull request #12640 from chef/INFC-7
Done
-rw-r--r--Gemfile.lock6
-rw-r--r--cspell.json1
-rw-r--r--docs/dev/how_to/building_chef_client_and_related_gems.md113
-rw-r--r--lib/chef/api_client_v1.rb10
-rw-r--r--lib/chef/client.rb114
-rw-r--r--lib/chef/event_dispatch/base.rb3
-rw-r--r--lib/chef/http/authenticator.rb100
-rw-r--r--spec/integration/client/client_spec.rb31
-rw-r--r--spec/unit/client_spec.rb52
-rw-r--r--spec/unit/http/authenticator_spec.rb67
10 files changed, 437 insertions, 60 deletions
diff --git a/Gemfile.lock b/Gemfile.lock
index 87bb2f4734..35a26ab383 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -226,6 +226,7 @@ GEM
fauxhai-ng (9.3.0)
net-ssh
ffi (1.15.5)
+ ffi (1.15.5-x64-mingw-ucrt)
ffi (1.15.5-x64-mingw32)
ffi (1.15.5-x86-mingw32)
ffi-libarchive (1.1.3)
@@ -321,7 +322,7 @@ GEM
net-ssh (6.1.0)
netrc (0.11.0)
nori (2.6.0)
- parallel (1.22.0)
+ parallel (1.22.1)
parser (3.1.1.0)
ast (~> 2.4.1)
parslet (1.8.2)
@@ -499,6 +500,7 @@ GEM
PLATFORMS
ruby
x64-mingw32
+ x64-unknown
x86-mingw32
DEPENDENCIES
@@ -525,4 +527,4 @@ DEPENDENCIES
webmock
BUNDLED WITH
- 2.3.5
+ 2.3.7
diff --git a/cspell.json b/cspell.json
index 9ca21c7297..767e66fffd 100644
--- a/cspell.json
+++ b/cspell.json
@@ -120,6 +120,7 @@
"borat",
"bsearch",
"bsevn",
+ "BSTR",
"bufptr",
"bufsize",
"bugfixing",
diff --git a/docs/dev/how_to/building_chef_client_and_related_gems.md b/docs/dev/how_to/building_chef_client_and_related_gems.md
new file mode 100644
index 0000000000..e8eafb2b55
--- /dev/null
+++ b/docs/dev/how_to/building_chef_client_and_related_gems.md
@@ -0,0 +1,113 @@
+# How to Build Chef Client and Associated Products
+
+
+
+This page endeavors to explain the vagaries of building out Chef Client and its accoutrements. The team is responsible for several gems in addition to the Chef Client codebase. We release the following gems:
+
+- Chef-Config
+- Chef-Utils
+- Chef-PowerShell
+- Knife
+- Chef-bin
+- Chef-Client
+
+
+
+### Contact Us: #eng-infra-chef in Slack
+
+**Codebase**: [chef-powershell-shim](https://github.com/chef/chef-powershell-shim)
+
+**Action**: Build
+
+**Access**: Github Actions
+
+**What to do**:
+
+When you peruse the repo you’ll note that there are a number of directories here. You will need to manage the accompanying .NET code bases in addition to the chef-powershell code itself.
+
+There are 2 ways to use this pipeline. The first is to merely merge your PR back into main once all the tests pass. This triggers an automated Github Actions pipeline that will push a compiled gem to RubyGems.org. The second way is to manually build and test the gem and then manually push it to Rubygems.org. You will need access to the RubyGems - chef-powershell repo via API key to do a manual upload. **NOTE**: This code base is entirely windows based. Meaning, you will compile and test on Windows and the gem will only run on a Windows device. The code supports PowerShell Core but the gcc libraries needed to run on Mac/Linux have not been built out yet.
+
+
+
+**Codebase**: [win32-certstore](https://github.com/chef/win32-certstore)
+
+**Access**: Pull Requests / Expeditor
+
+**What to do**:
+
+The Win32-Certstore code provides an FFI/Win32 based gem used to manage certificates on a Windows node.
+
+This one is a bit gnarly, see the notes below. Chef Infra team has backlogged issues to add functionality to the C++ libraries. If you have some spare cycles, we need the help.
+
+To start `git checkout -b`There are 2 parts of the code here. You’ll need to do your thing in C++ and then make sure the corresponding FFI/Ruby interfaces and methods are all working. Then push your branch back to main and open a PR. Once your code is green and merged, you’re done . **NOTE:** This code base is Windows based and contains C++ code. You’ll need a windows machine to test on. You can probably get away with developing on Mac or Linux.
+
+
+
+**Codebase**: [chef](https://github.com/chef/chef)
+
+**Action**: Promote a Build
+
+**Access**: Chef Internal Slack
+
+**What to do**:
+
+There are a few steps in performing a release. They are documented here : https://github.com/chef/chef/blob/main/docs/dev/how_to/releasing_chef_infra.md
+
+In Essence:
+
+1. Is your current build clean - no random errors, no busted tests?
+2. Have you documented all the changes in this build? This is a critical step to help customers and partner understand the changes we’re releasing.
+3. Promote the build
+4. Announce the build in Slack to #sous-chefs and #general
+5. Update Homebrew
+6. Update Chocolatey
+7. Backport to Chef 17 and Chef 16 as appropriate
+ 1. Git Pull Chef
+ 2. git checkout chef17
+ 3. git checkout -b my_branch_based_on_chef17
+ 4. Do my work on chef17 branch
+ 5. Merge it back to Chef17
+
+
+
+**Codebase**: [chef](https://github.com/chef/chef)
+
+**Action**: Build Chef Client
+
+**Access**: Pull Requests / Expeditor
+
+**What to do**:
+
+You have a feature or a bug you just fixed. Now what? Write your tests and, run rake to look for linting errors, spelling mistakes etc. Then push your branch back to main and create a pull request. This kicks off a build that will run your code against all 20 or so operating systems we support. Builds take a while. Once your build starts you have 2-3 hours or so to do something else. Once your build passes, get it approved and merged back to main. You’re done, unless you’re in charge of releases this week, in which case see the item just above about promoting builds
+
+
+
+**Codebase**: [chef](https://github.com/chef/chef)
+
+**Action**: Ad Hoc builds
+
+**Access**: Buildkite
+
+**What to do**:
+
+You have some code that may or may not really dodgy and you kinda need/want to see where the possible problems are with it. You can do an ad-hoc build against your branch to give it a go. To do that, you do this: There are 2 paths you can follow for a build. Chef stand-alone and Chef as part of the Chef Workstation product.
+
+[Chef Client Ad-Hoc Build Site](https://buildkite.com/chef/chef-chef-master-omnibus-adhoc/)
+
+[Chef Workstation Ad Hoc Build Site](https://buildkite.com/chef/chef-chef-workstation-master-omnibus-adhoc/)
+
+Steps:
+
+1. Click either link and if asked, confirm your login settings and then click the link in the verification email.
+
+2. Once past that you’ll need to add a ‘Pipeline’ - create a name for your pipeline and git it the root of github repo you want to build from
+
+3. Past that you’ll be asked to create a new build that uses your pipeline. Notice you can use any branch, you’ll enter yours here.
+
+4. You can use the options page to add environment variables that are unique to your build or do things like build only Windows nodes:
+
+ ```
+ OMNIBUS_BUILD_FILTER=windows*
+ ```
+
+
diff --git a/lib/chef/api_client_v1.rb b/lib/chef/api_client_v1.rb
index 6178cb91c3..51c915218e 100644
--- a/lib/chef/api_client_v1.rb
+++ b/lib/chef/api_client_v1.rb
@@ -64,6 +64,10 @@ class Chef
@chef_rest_v1 ||= Chef::ServerAPI.new(Chef::Config[:chef_server_url], { api_version: "1", inflate_json_class: false })
end
+ def chef_rest_v1_with_validator
+ @chef_rest_v1_with_validator ||= Chef::ServerAPI.new(Chef::Config[:chef_server_url], { client_name: Chef::Config[:validation_client_name], signing_key_filename: Chef::Config[:validation_key], api_version: "1", inflate_json_class: false })
+ end
+
def self.http_api
Chef::ServerAPI.new(Chef::Config[:chef_server_url], { api_version: "1", inflate_json_class: false })
end
@@ -293,7 +297,11 @@ class Chef
payload[:public_key] = public_key unless public_key.nil?
payload[:create_key] = create_key unless create_key.nil?
- new_client = chef_rest_v1.post("clients", payload)
+ new_client = if Chef::Config[:migrate_key_to_keystore] == true
+ chef_rest_v1_with_validator.post("clients", payload)
+ else
+ chef_rest_v1.post("clients", payload)
+ end
# get the private_key out of the chef_key hash if it exists
if new_client["chef_key"]
diff --git a/lib/chef/client.rb b/lib/chef/client.rb
index 5ec15fb582..07798798b9 100644
--- a/lib/chef/client.rb
+++ b/lib/chef/client.rb
@@ -64,6 +64,10 @@ class Chef
# The main object in a Chef run. Preps a Chef::Node and Chef::RunContext,
# syncs cookbooks if necessary, and triggers convergence.
class Client
+ CRYPT_EXPORTABLE = 0x00000001
+
+ attr_reader :local_context
+
extend Chef::Mixin::Deprecation
extend Forwardable
@@ -229,7 +233,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,16 +641,19 @@ class Chef
# @api private
#
def register(client_name = node_name, config = Chef::Config)
- if Chef::HTTP::Authenticator.detect_certificate_key(client_name)
- if File.exists?(config[:client_key])
- logger.warn("WARNING - Client key #{client_name} is present on disk, ignoring that in favor of key stored in CertStore")
- end
- 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]
+ if !config[:client_key]
events.skipping_registration(client_name, config)
logger.trace("Client key is unspecified - skipping registration")
+ elsif ::Chef::Config[:migrate_key_to_keystore] == true && ChefUtils.windows?
+ cert_name = "chef-#{client_name}"
+ result = check_certstore_for_key(cert_name)
+ if result.rassoc("#{cert_name}")
+ logger.trace("Client key #{config[:client_key]} is present in Certificate Store - skipping registration")
+ else
+ create_new_key_and_register(cert_name)
+ logger.trace("New client keys created in the Certificate Store - skipping registration")
+ end
+ events.skipping_registration(client_name, config)
elsif File.exists?(config[:client_key])
events.skipping_registration(client_name, config)
logger.trace("Client key #{config[:client_key]} is present - skipping registration")
@@ -665,6 +672,94 @@ class Chef
raise
end
+ # In the brave new world of No Certs On Disk, we want to put the pem file into Keychain or the Certstore
+ # But is it already there?
+ def check_certstore_for_key(cert_name)
+ require "win32-certstore"
+ win32certstore = ::Win32::Certstore.open("MY")
+ win32certstore.search("#{cert_name}")
+ end
+
+ def generate_pfx_package(cert_name, date)
+ self.class.generate_pfx_package(cert_name, date)
+ end
+
+ def self.generate_pfx_package(cert_name, date)
+ require "openssl" unless defined?(OpenSSL)
+
+ key = OpenSSL::PKey::RSA.new(2048)
+ public_key = key.public_key
+
+ subject = "CN=#{cert_name}"
+
+ cert = OpenSSL::X509::Certificate.new
+ cert.subject = cert.issuer = OpenSSL::X509::Name.parse(subject)
+ cert.not_before = Time.now
+ cert.not_after = Time.parse(date)
+ cert.public_key = public_key
+ cert.serial = 0x0
+ cert.version = 2
+
+ ef = OpenSSL::X509::ExtensionFactory.new
+ ef.subject_certificate = cert
+ ef.issuer_certificate = cert
+ cert.extensions = [
+ ef.create_extension("subjectKeyIdentifier", "hash"),
+ ef.create_extension("keyUsage", "digitalSignature,keyEncipherment", true),
+ ]
+ cert.add_extension(ef.create_ext_from_string("extendedKeyUsage=critical,serverAuth,clientAuth"))
+
+ cert.sign key, OpenSSL::Digest.new("SHA256")
+ password = ::Chef::HTTP::Authenticator.get_cert_password
+ pfx = OpenSSL::PKCS12.create(password, subject, key, cert)
+ pfx
+ end
+
+ def create_new_key_and_register(cert_name)
+ require "time" unless defined?(Time)
+ autoload :URI, "uri"
+
+ # KeyMigration.instance.key_migrated = true
+
+ node = Chef::Config[:node_name]
+ d = Time.now
+ if d.month == 10 || d.month == 11 || d.month == 12
+ end_date = Time.new(d.year + 1, d.month - 9, d.day, d.hour, d.min, d.sec).utc.iso8601
+ else
+ end_date = Time.new(d.year, d.month + 3, d.day, d.hour, d.min, d.sec).utc.iso8601
+ end
+
+ payload = {
+ name: node,
+ clientname: node,
+ public_key: "",
+ expiration_date: end_date,
+ }
+
+ new_pfx = generate_pfx_package(cert_name, end_date)
+ payload[:public_key] = new_pfx.certificate.public_key.to_pem
+ base_url = "#{Chef::Config[:chef_server_url]}"
+ client = Chef::ServerAPI.new(base_url, client_name: Chef::Config[:validation_client_name], signing_key_filename: Chef::Config[:validation_key])
+ client.post(base_url + "/clients", payload)
+ Chef::Log.trace("Updated client data: #{client.inspect}")
+ import_pfx_to_store(new_pfx)
+ end
+
+ def import_pfx_to_store(new_pfx)
+ self.class.import_pfx_to_store(new_pfx)
+ end
+
+ def self.import_pfx_to_store(new_pfx)
+ password = ::Chef::HTTP::Authenticator.get_cert_password
+ require "win32-certstore"
+ tempfile = Tempfile.new("#{Chef::Config[:node_name]}.pfx")
+ File.open(tempfile, "wb") { |f| f.print new_pfx.to_der }
+
+ store = ::Win32::Certstore.open("MY")
+ store.add_pfx(tempfile, password, CRYPT_EXPORTABLE)
+ tempfile.unlink
+ end
+
#
# Converges all compiled resources.
#
@@ -929,3 +1024,4 @@ end
require_relative "cookbook_loader"
require_relative "cookbook_version"
require_relative "cookbook/synchronizer"
+
diff --git a/lib/chef/event_dispatch/base.rb b/lib/chef/event_dispatch/base.rb
index a973c31612..669c1d6286 100644
--- a/lib/chef/event_dispatch/base.rb
+++ b/lib/chef/event_dispatch/base.rb
@@ -273,6 +273,9 @@ class Chef
# Called if the converge phase fails
def converge_failed(exception); end
+ # Called when migrating from a pem on disk to a pem stored in Keychain or Windows Certstore
+ def key_migration_status(key_migrated = false); end
+
# TODO: need events for notification resolve?
# def notifications_resolved
# end
diff --git a/lib/chef/http/authenticator.rb b/lib/chef/http/authenticator.rb
index 26413c283b..3b9d49f42a 100644
--- a/lib/chef/http/authenticator.rb
+++ b/lib/chef/http/authenticator.rb
@@ -100,6 +100,18 @@ class Chef
self.class.retrieve_certificate_key(client_name)
end
+ def get_cert_password
+ self.class.get_cert_password
+ end
+
+ def encrypt_pfx_pass
+ self.class.encrypt_pfx_pass
+ end
+
+ def decrypt_pfx_pass
+ self.class.decrypt_pfx_pass
+ 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
@@ -115,7 +127,7 @@ class Chef
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
+ $cert = Get-ChildItem -path cert:\\LocalMachine\\My -Recurse -Force | Where-Object { $_.Subject -Match "chef-#{client_name}" } -ErrorAction Stop
if (($cert.HasPrivateKey -eq $true) -and ($cert.PrivateKey.Key.ExportPolicy -ne "NonExportable")) {
return $true
}
@@ -129,17 +141,16 @@ class Chef
def load_signing_key(key_file, raw_key = nil)
results = retrieve_certificate_key(Chef::Config[:node_name])
- if key_file == nil? && raw_key == nil?
- puts "\nNo key detected\n"
- elsif !!results
- # results variable can be 1 of 2 values - "False" or the contents of a key.
+ if !!results
@raw_key = results
+ elsif key_file == nil? && raw_key == nil?
+ puts "\nNo key detected\n"
elsif !!key_file
@raw_key = IO.read(key_file).strip
elsif !!raw_key
@raw_key = raw_key.strip
else
- return nil
+ return
end
# Pass in '' as the passphrase to avoid OpenSSL prompting on the TTY if
# given an encrypted key. This also helps if using a single file for
@@ -154,7 +165,6 @@ 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"
@@ -166,60 +176,86 @@ class Chef
present.each do |secret|
if secret[:name] == "PfxPass"
- return secret[:data]
+ password = decrypt_pfx_pass(secret[:data])
+ return password
end
end
- # if we make it this far, that means there is no valid password in the Registry. Fail out to correct that.
raise Chef::Exceptions::Win32RegKeyMissing
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"
+ Chef::Log.warn "Authentication Hive and values not present in registry, creating them now"
new_path = "HKEY_LOCAL_MACHINE\\Software\\Progress\\Authentication"
unless @win32registry.key_exists?(new_path)
@win32registry.create_key(new_path, true)
end
- password = SOME_CHARS.sample(1 + rand(SOME_CHARS.count)).join[0...14]
- values = { name: "PfxPass", type: :string, data: password }
+ size = 14
+ password = SOME_CHARS.sample(size).join
+ encrypted_pass = encrypt_pfx_pass(password)
+ values = { name: "PfxPass", type: :string, data: encrypted_pass }
@win32registry.set_value(new_path, values)
password
end
+ def self.encrypt_pfx_pass(password)
+ powershell_code = <<~CODE
+ $encrypted_string = ConvertTo-SecureString "#{password}" -AsPlainText -Force
+ $secure_string = ConvertFrom-SecureString $encrypted_string
+ return $secure_string
+ CODE
+ powershell_exec!(powershell_code).result
+ end
+
+ def self.decrypt_pfx_pass(password)
+ powershell_code = <<~CODE
+ $secure_string = "#{password}" | ConvertTo-SecureString
+ $string = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR((($secure_string))))
+ return $string
+ CODE
+ powershell_exec!(powershell_code).result
+ end
+
def self.retrieve_certificate_key(client_name)
require "openssl" unless defined?(OpenSSL)
if ChefUtils.windows?
-
password = get_cert_password
-
return false unless password
if check_certstore_for_key(client_name)
- 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
- pkcs = OpenSSL::PKCS12.new(File.binread(my_result["PSPath"].split("::")[1]), password)
- ::File.delete(my_result["PSPath"].split("::")[1])
- return OpenSSL::PKey::RSA.new(pkcs.key.to_pem)
- end
+ ps_blob = powershell_exec!(get_the_key_ps(client_name, password)).result
+ file_path = ps_blob["PSPath"].split("::")[1]
+ pkcs = OpenSSL::PKCS12.new(File.binread(file_path), password)
+
+ # We test the pfx we just extracted the private key from
+ # if that cert is expiring in 7 days or less we generate a new pfx/p12 object
+ # then we post the new public key from that to the client endpoint on
+ # chef server.
+ # is_certificate_expiring(pkcs)
+ File.delete(file_path)
+
+ return pkcs.key.private_to_pem
end
end
false
end
+ def self.get_the_key_ps(client_name, password)
+ powershell_code = <<~CODE
+ Try {
+ $my_pwd = ConvertTo-SecureString -String "#{password}" -Force -AsPlainText;
+ $cert = Get-ChildItem -path cert:\\LocalMachine\\My -Recurse | Where-Object { $_.Subject -match "chef-#{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
+ end
+
def authentication_headers(method, url, json_body = nil, headers = nil)
request_params = {
http_method: method,
diff --git a/spec/integration/client/client_spec.rb b/spec/integration/client/client_spec.rb
index 9bf7497a9e..3960123acd 100644
--- a/spec/integration/client/client_spec.rb
+++ b/spec/integration/client/client_spec.rb
@@ -35,21 +35,22 @@ describe "chef-client" do
@server = @api = nil
end
- def install_certificate_in_store
+ def install_certificate_in_store(client_name)
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
- @win32registry = Chef::Win32::Registry.new
- path = "HKEY_LOCAL_MACHINE\\Software\\Progress\\Authentication"
- unless @win32registry.key_exists?(path)
- @win32registry.create_key(path, true)
- end
- password = SOME_CHARS.sample(1 + rand(SOME_CHARS.count)).join[0...14]
- values = { name: "PfxPass", type: :string, data: password }
- @win32registry.set_value(path, values)
+ ::Chef::HTTP::Authenticator.get_cert_password
+ # @win32registry = Chef::Win32::Registry.new
+ # path = "HKEY_LOCAL_MACHINE\\Software\\Progress\\Authentication"
+ # unless @win32registry.key_exists?(path)
+ # @win32registry.create_key(path, true)
+ # end
+ # password = SOME_CHARS.sample(1 + rand(SOME_CHARS.count)).join[0...14]
+ # values = { name: "PfxPass", type: :string, data: password }
+ # @win32registry.set_value(path, values)
end
def remove_certificate_from_store
@@ -59,7 +60,7 @@ describe "chef-client" do
end
def remove_registry_key
- powershell_exec!("Remove-Item -Path HKLM:\\SOFTWARE\\Progress -Recurse")
+ powershell_exec!("Remove-ItemProperty -Path HKLM:\\SOFTWARE\\Progress\\Authentication -Name 'PfxPass' ")
end
def verify_export_password_exists
@@ -92,6 +93,7 @@ describe "chef-client" do
let(:chef_client) { "bundle exec #{ChefUtils::Dist::Infra::CLIENT} --minimal-ohai --always-dump-stacktrace" }
let(:chef_solo) { "bundle exec #{ChefUtils::Dist::Solo::EXEC} --legacy-mode --minimal-ohai --always-dump-stacktrace" }
let(:client_name) { "chef-973334" }
+ let(:hostname) { "973334" }
context "when validation.pem in current Directory" do
let(:validation_path) { "" }
@@ -178,7 +180,6 @@ describe "chef-client" do
# FATAL: Configuration error NoMethodError: undefined method `xxx' for nil:NilClass
expect(result.stdout).to include("xxx")
end
-
end
it "should complete with success" do
@@ -194,19 +195,17 @@ describe "chef-client" do
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
+ install_certificate_in_store(client_name)
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.check_certstore_for_key(client_name)).to eq(true)
+ expect(Chef::HTTP::Authenticator.check_certstore_for_key(hostname)).to eq(true)
end
it "should verify that the export password for the pfx is loaded in the Registry" do
@@ -214,7 +213,7 @@ describe "chef-client" do
end
it "should verify that a private key is returned to me" do
- expect(Chef::HTTP::Authenticator.retrieve_certificate_key(client_name)).not_to be_falsey
+ expect(Chef::HTTP::Authenticator.retrieve_certificate_key(client_name)).not_to be nil
end
end
end
diff --git a/spec/unit/client_spec.rb b/spec/unit/client_spec.rb
index 1e835ae398..7f02eacb39 100644
--- a/spec/unit/client_spec.rb
+++ b/spec/unit/client_spec.rb
@@ -23,6 +23,11 @@ require "chef/run_context"
require "chef/server_api"
require "rbconfig"
+begin
+ require "chef-powershell"
+rescue LoadError
+end
+
class FooError < RuntimeError
end
@@ -282,6 +287,53 @@ shared_examples "a failed run" do
end
end
+describe Chef::Client, :windows_only do
+ let(:hostname) { "test" }
+ let(:my_client) { Chef::Client.new }
+ let(:cert_name) { "chef-#{hostname}" }
+ let(:node_name) { "#{hostname}" }
+ let(:end_date) do
+ d = Time.now
+ if d.month == 10 || d.month == 11 || d.month == 12
+ end_date = Time.new(d.year + 1, d.month - 9, d.day, d.hour, d.min, d.sec).utc.iso8601
+ else
+ end_date = Time.new(d.year, d.month + 3, d.day, d.hour, d.min, d.sec).utc.iso8601
+ end
+ end
+ # include_context "client"
+ before(:each) do
+ Chef::Config[:migrate_key_to_keystore] = true
+ end
+
+ after(:each) do
+ delete_certificate(cert_name)
+ end
+
+ context "when the client intially boots the first time" do
+ it "verfies that a certificate was correctly created and exists in the Cert Store" do
+ new_pfx = my_client.generate_pfx_package(cert_name, end_date)
+ my_client.import_pfx_to_store(new_pfx)
+ expect(my_client.check_certstore_for_key(cert_name)).not_to be false
+ end
+
+ it "correctly returns a new Publc Key" do
+ new_pfx = my_client.generate_pfx_package(cert_name, end_date)
+ cert_object = new_pfx.certificate.public_key.to_pem
+ expect(cert_object.to_s).to match(/PUBLIC KEY/)
+ end
+
+ end
+
+ def delete_certificate(cert_name)
+ require "chef/mixin/powershell_exec"
+ extend Chef::Mixin::PowershellExec
+ powershell_code = <<~CODE
+ Get-ChildItem -path cert:\\LocalMachine\\My -Recurse -Force | Where-Object { $_.Subject -Match "#{cert_name}" } | Remove-item
+ CODE
+ powershell_exec!(powershell_code)
+ end
+end
+
describe Chef::Client do
include_context "client"
diff --git a/spec/unit/http/authenticator_spec.rb b/spec/unit/http/authenticator_spec.rb
index 4f43c19520..a1b67610c5 100644
--- a/spec/unit/http/authenticator_spec.rb
+++ b/spec/unit/http/authenticator_spec.rb
@@ -19,6 +19,69 @@
require "spec_helper"
require "chef/http/authenticator"
+describe Chef::HTTP::Authenticator, :windows_only do
+ let(:class_instance) { Chef::HTTP::Authenticator.new(client_name: "test") }
+ let(:method) { "GET" }
+ let(:url) { URI("https://chef.example.com/organizations/test") }
+ let(:headers) { {} }
+ let(:data) { "" }
+ let(:node_name) { "test" }
+ let(:passwrd) { "some_insecure_password" }
+
+ before do
+ Chef::Config[:node_name] = node_name
+ cert_name = "chef-#{node_name}"
+ d = Time.now
+ end_date = Time.new(d.year, d.month + 3, d.day, d.hour, d.min, d.sec).utc.iso8601
+
+ my_client = Chef::Client.new
+ pfx = my_client.generate_pfx_package(cert_name, end_date)
+ my_client.import_pfx_to_store(pfx)
+ end
+
+ after(:each) do
+ require "chef/mixin/powershell_exec"
+ extend Chef::Mixin::PowershellExec
+ cert_name = "chef-#{node_name}"
+ delete_certificate(cert_name)
+ end
+
+ context "when retrieving a certificate from the certificate store" do
+ it "retrieves a certificate password from the registry when the hive does not already exist" do
+ delete_registry_hive
+ expect { class_instance.get_cert_password }.not_to raise_error
+ end
+
+ it "should return a password of at least 14 characters in length" do
+ password = class_instance.get_cert_password
+ expect(password.length).to eql(14)
+ end
+
+ it "correctly retrieves a valid certificate in pem format from the certstore" do
+ require "openssl"
+ certificate = class_instance.retrieve_certificate_key(node_name)
+ cert_object = OpenSSL::PKey::RSA.new(certificate)
+ expect(cert_object.to_s).to match(/BEGIN RSA PRIVATE KEY/)
+ end
+ end
+
+ def delete_certificate(cert_name)
+ powershell_code = <<~CODE
+ Get-ChildItem -path cert:\\LocalMachine\\My -Recurse -Force | Where-Object { $_.Subject -Match "#{cert_name}" } | Remove-item
+ CODE
+ powershell_exec!(powershell_code)
+ end
+
+ def delete_registry_hive
+ @win32registry = Chef::Win32::Registry.new
+ path = "HKEY_LOCAL_MACHINE\\Software\\Progress\\Authentication"
+ present = @win32registry.get_values(path)
+ unless present.nil? || present.empty?
+ @win32registry.delete_key(path, true)
+ end
+ end
+end
+
describe Chef::HTTP::Authenticator do
let(:class_instance) { Chef::HTTP::Authenticator.new(client_name: "test") }
let(:method) { "GET" }
@@ -26,6 +89,10 @@ describe Chef::HTTP::Authenticator do
let(:headers) { {} }
let(:data) { "" }
+ before do
+ ::Chef::Config[:node_name] = "foo"
+ end
+
context "when handle_request is called" do
shared_examples_for "merging the server API version into the headers" do
before do