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


126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/chef/http/authenticator.rb', line 126

def self.check_certstore_for_key(client_name)
  powershell_code = <<~CODE
    $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
    }
    else{
      return $false
    }
  CODE
  powershell_exec!(powershell_code).result
end

.decrypt_pfx_pass(password) ⇒ Object


209
210
211
212
213
214
215
216
# File 'lib/chef/http/authenticator.rb', line 209

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

.delete_old_key_ps(client_name) ⇒ Object


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

def self.delete_old_key_ps(client_name)
  powershell_code = <<~CODE
    Get-ChildItem -path cert:\\LocalMachine\\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(password) ⇒ Object


200
201
202
203
204
205
206
207
# File 'lib/chef/http/authenticator.rb', line 200

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

.get_cert_passwordObject


166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
# File 'lib/chef/http/authenticator.rb', line 166

def self.get_cert_password
  @win32registry = Chef::Win32::Registry.new
  path = "HKEY_LOCAL_MACHINE\\Software\\Progress\\Authentication"
  # does the registry key even exist?
  present = @win32registry.get_values(path)
  if present.nil? || present.empty?
    raise Chef::Exceptions::Win32RegKeyMissing
  end

  present.each do |secret|
    if secret[:name] == "PfxPass"
      password = decrypt_pfx_pass(secret[:data])
      return password
    end
  end

  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 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
  require "securerandom" unless defined?(SecureRandom)
  size = 14
  password = SecureRandom.alphanumeric(size)
  encrypted_pass = encrypt_pfx_pass(password)
  values = { name: "PfxPass", type: :string, data: encrypted_pass }
  @win32registry.set_value(new_path, values)
  password
end

.get_the_key_ps(client_name, password) ⇒ Object


254
255
256
257
258
259
260
261
262
263
264
265
266
# File 'lib/chef/http/authenticator.rb', line 254

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

.is_certificate_expiring?(pkcs) ⇒ Boolean

Returns:

  • (Boolean)

248
249
250
251
252
# File 'lib/chef/http/authenticator.rb', line 248

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

.retrieve_certificate_key(client_name) ⇒ Object


218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
# File 'lib/chef/http/authenticator.rb', line 218

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)
      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


274
275
276
277
278
279
280
281
282
283
284
# File 'lib/chef/http/authenticator.rb', line 274

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_passObject


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

def decrypt_pfx_pass
  self.class.decrypt_pfx_pass
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_passObject


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

def encrypt_pfx_pass
  self.class.encrypt_pfx_pass
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


139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/chef/http/authenticator.rb', line 139

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