Class: Chef::HTTP::Authenticator

Inherits:
Object
  • Object
show all
Extended by:
Mixin::PowershellExec
Defined in:
lib/chef/http/authenticator.rb

Constant Summary collapse

DEFAULT_SERVER_API_VERSION =
"2".freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(opts = {}) ⇒ Authenticator

Returns a new instance of Authenticator.



41
42
43
44
45
46
47
48
49
# File 'lib/chef/http/authenticator.rb', line 41

def initialize(opts = {})
  @raw_key = nil
  @sign_request = true
  @signing_key_filename = opts[:signing_key_filename]
  @key = load_signing_key(opts[:signing_key_filename], opts[:raw_key])
  @auth_credentials = AuthCredentials.new(opts[:client_name], @key, use_ssh_agent: opts[:ssh_agent_signing])
  @version_class = opts[:version_class]
  @api_version = opts[:api_version]
end

Instance Attribute Details

#api_versionObject (readonly)

Returns the value of attribute api_version.



37
38
39
# File 'lib/chef/http/authenticator.rb', line 37

def api_version
  @api_version
end

#attr_namesObject (readonly)

Returns the value of attribute attr_names.



34
35
36
# File 'lib/chef/http/authenticator.rb', line 34

def attr_names
  @attr_names
end

#auth_credentialsObject (readonly)

Returns the value of attribute auth_credentials.



35
36
37
# File 'lib/chef/http/authenticator.rb', line 35

def auth_credentials
  @auth_credentials
end

#raw_keyObject (readonly)

Returns the value of attribute raw_key.



33
34
35
# File 'lib/chef/http/authenticator.rb', line 33

def raw_key
  @raw_key
end

#sign_requestObject

Returns the value of attribute sign_request.



39
40
41
# File 'lib/chef/http/authenticator.rb', line 39

def sign_request
  @sign_request
end

#signing_key_filenameObject (readonly)

Returns the value of attribute signing_key_filename.



32
33
34
# File 'lib/chef/http/authenticator.rb', line 32

def signing_key_filename
  @signing_key_filename
end

#version_classObject (readonly)

Returns the value of attribute version_class.



36
37
38
# File 'lib/chef/http/authenticator.rb', line 36

def version_class
  @version_class
end

Class Method Details

.check_certstore_for_key(client_name) ⇒ Object



134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/chef/http/authenticator.rb', line 134

def self.check_certstore_for_key(client_name)
  store = get_cert_user
  powershell_code = <<~CODE
    $cert = Get-ChildItem -path cert:\\#{store}\\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
    }
    else{
      return $false
    }
  CODE
  powershell_exec!(powershell_code).result
end

.create_and_store_new_password(password = nil) ⇒ Object

This method name is a bit of a misnomer. We call it to legit create a new password using the vector format. But we also call it with a password that needs to be migrated to use the vector format too.



276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
# File 'lib/chef/http/authenticator.rb', line 276

def self.create_and_store_new_password(password = nil)
  @win32registry = Chef::Win32::Registry.new
  store = get_registry_user
  path = "#{store}\\Software\\Progress\\Authentication"
  if password.nil?
    require "securerandom" unless defined?(SecureRandom)
    size = 14
    password = SecureRandom.alphanumeric(size)
  end
  encrypted_blob = encrypt_pfx_pass_with_vector(password)
  encrypted_password = encrypted_blob[0]
  key = encrypted_blob[1]
  vector = encrypted_blob[2]
  values = [
            { name: "PfxPass", type: :string, data: encrypted_password },
            { name: "PfxKey", type: :string, data: key },
            { name: "PfxIV", type: :string, data: vector },
          ]
  values.each do |i|
    @win32registry.set_value(path, i)
  end
  password
end

.decrypt_pfx_pass_with_password(password_blob) ⇒ Object



247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
# File 'lib/chef/http/authenticator.rb', line 247

def self.decrypt_pfx_pass_with_password(password_blob)
  password = ""
  password_blob.each do |secret|
    if secret[:name] == "PfxPass"
      password = secret[:data]
    else
      Chef::Log.error("Failed to retrieve a password for the private key")
    end
  end
  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

.decrypt_pfx_pass_with_vector(password_blob) ⇒ Object



226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
# File 'lib/chef/http/authenticator.rb', line 226

def self.decrypt_pfx_pass_with_vector(password_blob)
  raw_data = password_blob.map { |x| x[:data] }
  password = raw_data[0]
  key = raw_data[1]
  vector = raw_data[2]

  powershell_code = <<~CODE
    $KeyBytes = [System.Convert]::FromBase64String("#{key}")
    $IVBytes = [System.Convert]::FromBase64String("#{vector}")
    $aes = [System.Security.Cryptography.Aes]::Create()
    $aes.Key = $KeyBytes
    $aes.IV = $IVBytes
    $EncryptedBytes = [System.Convert]::FromBase64String("#{password}")
    $Decryptor = $aes.CreateDecryptor()
    $DecryptedBytes = $Decryptor.TransformFinalBlock($EncryptedBytes,0,$EncryptedBytes.Length)
    $DecryptedString = [System.Text.Encoding]::Unicode.GetString($DecryptedBytes)
    return $DecryptedString
  CODE
  results = powershell_exec!(powershell_code).result
end

.delete_old_key_ps(client_name) ⇒ Object



350
351
352
353
354
355
# File 'lib/chef/http/authenticator.rb', line 350

def self.delete_old_key_ps(client_name)
  store = get_cert_user
  powershell_code = <<~CODE
    Get-ChildItem -path cert:\\#{store}\\My -Recurse | Where-Object { $_.Subject -match "chef-#{client_name}$" } | Remove-Item -ErrorAction Stop;
  CODE
end

.detect_certificate_key(client_name) ⇒ Object

Detects if a private key exists in a certificate repository like Keychain (macOS) or Certificate Store (Windows)

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

Parameters:

  • client_name
    • we're using the node name to store and retrieve any keys


118
119
120
121
122
123
124
# File 'lib/chef/http/authenticator.rb', line 118

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

.encrypt_pfx_pass_with_vector(password) ⇒ Object



210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/chef/http/authenticator.rb', line 210

def self.encrypt_pfx_pass_with_vector(password)
  powershell_code = <<~CODE
    $AES = [System.Security.Cryptography.Aes]::Create()
    $key_temp = [System.Convert]::ToBase64String($AES.Key)
    $iv_temp = [System.Convert]::ToBase64String($AES.IV)
    $encryptor = $AES.CreateEncryptor()
    [System.Byte[]]$Bytes =  [System.Text.Encoding]::Unicode.GetBytes("#{password}")
    $EncryptedBytes = $encryptor.TransformFinalBlock($Bytes,0,$Bytes.Length)
    $EncryptedBase64String = [System.Convert]::ToBase64String($EncryptedBytes)
    # create array of encrypted pass, key, iv
    $password_blob = @($EncryptedBase64String, $key_temp, $iv_temp)
    return $password_blob
  CODE
  powershell_exec!(powershell_code).result
end

.get_cert_passwordObject



175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/chef/http/authenticator.rb', line 175

def self.get_cert_password
  store = get_registry_user
  @win32registry = Chef::Win32::Registry.new
  path = "#{store}\\Software\\Progress\\Authentication"
  # does the registry key even exist?
  # password_blob should be an array of hashes
  password_blob = @win32registry.get_values(path)
  if password_blob.nil? || password_blob.empty?
    raise Chef::Exceptions::Win32RegKeyMissing
  end

  # Did someone have just the password stored in the registry?
  raw_data = password_blob.map { |x| x[:data] }
  vector = raw_data[2]
  if !!vector
    decrypted_password = decrypt_pfx_pass_with_vector(password_blob)
  else
    decrypted_password = decrypt_pfx_pass_with_password(password_blob)
    if !!decrypted_password
      migrate_pass_to_use_vector(decrypted_password)
    else
      Chef::Log.error("Failed to retrieve certificate password")
    end
  end
  decrypted_password
rescue Chef::Exceptions::Win32RegKeyMissing
  # if we don't have a password, log that and generate one
  store = get_registry_user
  new_path = "#{store}\\Software\\Progress\\Authentication"
  unless @win32registry.key_exists?(new_path)
    @win32registry.create_key(new_path, true)
  end
  create_and_store_new_password
end

.get_cert_userObject



126
127
128
# File 'lib/chef/http/authenticator.rb', line 126

def self.get_cert_user
  Chef::Config[:auth_key_registry_type] == "user" ? "CurrentUser" : "LocalMachine"
end

.get_registry_userObject



130
131
132
# File 'lib/chef/http/authenticator.rb', line 130

def self.get_registry_user
  Chef::Config[:auth_key_registry_type] == "user" ? "HKEY_CURRENT_USER" : "HKEY_LOCAL_MACHINE"
end

.get_the_key_ps(client_name, password) ⇒ Object



335
336
337
338
339
340
341
342
343
344
345
346
347
348
# File 'lib/chef/http/authenticator.rb', line 335

def self.get_the_key_ps(client_name, password)
  store = get_cert_user
  powershell_code = <<~CODE
      Try {
        $my_pwd = ConvertTo-SecureString -String "#{password}" -Force -AsPlainText;
        $cert = Get-ChildItem -path cert:\\#{store}\\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

.is_certificate_expiring?(pkcs) ⇒ Boolean

Returns:

  • (Boolean)


329
330
331
332
333
# File 'lib/chef/http/authenticator.rb', line 329

def self.is_certificate_expiring?(pkcs)
  today = Date.parse(Time.now.utc.iso8601)
  future = Date.parse(pkcs.certificate.not_after.iso8601)
  future.mjd - today.mjd <= 7
end

.migrate_pass_to_use_vector(password) ⇒ Object



264
265
266
267
268
269
270
271
272
# File 'lib/chef/http/authenticator.rb', line 264

def self.migrate_pass_to_use_vector(password)
  store = get_cert_user
  corrected_store = (store == "CurrentUser" ? "HKCU" : "HKLM")
  powershell_code = <<~CODE
    Remove-ItemProperty -Path "#{corrected_store}:\\Software\\Progress\\Authentication" -Name "PfXPass"
  CODE
  powershell_exec!(powershell_code)
  create_and_store_new_password(password)
end

.retrieve_certificate_key(client_name) ⇒ Object



300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
# File 'lib/chef/http/authenticator.rb', line 300

def self.retrieve_certificate_key(client_name)
  if ChefUtils.windows?
    require "openssl" unless defined?(OpenSSL)
    password = get_cert_password
    return false unless password

    if !!check_certstore_for_key(client_name)
      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 check 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.
      File.delete(file_path)
      key_expiring = is_certificate_expiring?(pkcs)
      if key_expiring
        powershell_exec!(delete_old_key_ps(client_name))
        ::Chef::Client.update_key_and_register(Chef::Config[:client_name], pkcs)
      end

      return pkcs.key.private_to_pem
    end
  end

  false
end

Instance Method Details

#authentication_headers(method, url, json_body = nil, headers = nil) ⇒ Object



357
358
359
360
361
362
363
364
365
366
367
# File 'lib/chef/http/authenticator.rb', line 357

def authentication_headers(method, url, json_body = nil, headers = nil)
  request_params = {
    http_method: method,
    path: url.path,
    body: json_body,
    host: "#{url.host}:#{url.port}",
    headers: headers,
  }
  request_params[:body] ||= ""
  auth_credentials.signature_headers(request_params)
end

#check_certstore_for_key(client_name) ⇒ Object



93
94
95
# File 'lib/chef/http/authenticator.rb', line 93

def check_certstore_for_key(client_name)
  self.class.check_certstore_for_key(client_name)
end

#client_nameObject



85
86
87
# File 'lib/chef/http/authenticator.rb', line 85

def client_name
  @auth_credentials.client_name
end

#decrypt_pfx_pass_with_vectorObject



109
110
111
# File 'lib/chef/http/authenticator.rb', line 109

def decrypt_pfx_pass_with_vector
  self.class.decrypt_pfx_pass_with_vector
end

#detect_certificate_key(client_name) ⇒ Object



89
90
91
# File 'lib/chef/http/authenticator.rb', line 89

def detect_certificate_key(client_name)
  self.class.detect_certificate_key(client_name)
end

#encrypt_pfx_pass_with_vectorObject



105
106
107
# File 'lib/chef/http/authenticator.rb', line 105

def encrypt_pfx_pass_with_vector
  self.class.encrypt_pfx_pass_with_vector
end

#get_cert_passwordObject



101
102
103
# File 'lib/chef/http/authenticator.rb', line 101

def get_cert_password
  self.class.get_cert_password
end

#handle_request(method, url, headers = {}, data = false) ⇒ Object



51
52
53
54
55
# File 'lib/chef/http/authenticator.rb', line 51

def handle_request(method, url, headers = {}, data = false)
  headers["X-Ops-Server-API-Version"] = request_version
  headers.merge!(authentication_headers(method, url, data, headers)) if sign_requests?
  [method, url, headers, data]
end

#handle_response(http_response, rest_request, return_value) ⇒ Object



57
58
59
# File 'lib/chef/http/authenticator.rb', line 57

def handle_response(http_response, rest_request, return_value)
  [http_response, rest_request, return_value]
end

#handle_stream_complete(http_response, rest_request, return_value) ⇒ Object



65
66
67
# File 'lib/chef/http/authenticator.rb', line 65

def handle_stream_complete(http_response, rest_request, return_value)
  [http_response, rest_request, return_value]
end

#load_signing_key(key_file, raw_key = nil) ⇒ Object



148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/chef/http/authenticator.rb', line 148

def load_signing_key(key_file, raw_key = nil)
  results = retrieve_certificate_key(Chef::Config[:node_name])

  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
  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
  # both the public and private key with ssh-agent mode.
  @key = OpenSSL::PKey::RSA.new(@raw_key, "")
rescue SystemCallError, IOError => e
  Chef::Log.warn "Failed to read the private key #{key_file}: #{e.inspect}"
  raise Chef::Exceptions::PrivateKeyMissing, "I cannot read #{key_file}, which you told me to use to sign requests!"
rescue OpenSSL::PKey::RSAError
  msg = "The file #{key_file} or :raw_key option does not contain a correctly formatted private key or the key is encrypted.\n"
  msg << "The key file should begin with '-----BEGIN RSA PRIVATE KEY-----' and end with '-----END RSA PRIVATE KEY-----'"
  raise Chef::Exceptions::InvalidPrivateKey, msg
end

#request_versionObject



69
70
71
72
73
74
75
76
77
78
79
# File 'lib/chef/http/authenticator.rb', line 69

def request_version
  if version_class
    version_class.best_request_version
  elsif api_version
    api_version
  elsif Chef::ServerAPIVersions.instance.negotiated?
    Chef::ServerAPIVersions.instance.max_server_version.to_s
  else
    DEFAULT_SERVER_API_VERSION
  end
end

#retrieve_certificate_key(client_name) ⇒ Object



97
98
99
# File 'lib/chef/http/authenticator.rb', line 97

def retrieve_certificate_key(client_name)
  self.class.retrieve_certificate_key(client_name)
end

#sign_requests?Boolean

Returns:

  • (Boolean)


81
82
83
# File 'lib/chef/http/authenticator.rb', line 81

def sign_requests?
  auth_credentials.sign_requests? && @sign_request
end

#stream_response_handler(response) ⇒ Object



61
62
63
# File 'lib/chef/http/authenticator.rb', line 61

def stream_response_handler(response)
  nil
end