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
SOME_CHARS =

cspell:disable-next-line

"[email protected]#%^&*_-+=`|\\(){}[<]:;'>,.?/0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz".each_char.to_a

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(opts = {}) ⇒ Authenticator

Returns a new instance of Authenticator.


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

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.


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

def api_version
  @api_version
end

#attr_namesObject (readonly)

Returns the value of attribute attr_names.


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

def attr_names
  @attr_names
end

#auth_credentialsObject (readonly)

Returns the value of attribute auth_credentials.


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

def auth_credentials
  @auth_credentials
end

#raw_keyObject (readonly)

Returns the value of attribute raw_key.


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

def raw_key
  @raw_key
end

#sign_requestObject

Returns the value of attribute sign_request.


41
42
43
# File 'lib/chef/http/authenticator.rb', line 41

def sign_request
  @sign_request
end

#signing_key_filenameObject (readonly)

Returns the value of attribute signing_key_filename.


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

def signing_key_filename
  @signing_key_filename
end

#version_classObject (readonly)

Returns the value of attribute version_class.


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

def version_class
  @version_class
end

Class Method Details

.check_certstore_for_key(client_name) ⇒ Object


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

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


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

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

.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

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

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


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

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


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
199
# File 'lib/chef/http/authenticator.rb', line 168

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

.get_the_key_ps(client_name, password) ⇒ Object


245
246
247
248
249
250
251
252
253
254
255
256
257
# File 'lib/chef/http/authenticator.rb', line 245

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

.retrieve_certificate_key(client_name) ⇒ Object


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
# File 'lib/chef/http/authenticator.rb', line 219

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

Instance Method Details

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


259
260
261
262
263
264
265
266
267
268
269
# File 'lib/chef/http/authenticator.rb', line 259

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


95
96
97
# File 'lib/chef/http/authenticator.rb', line 95

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

#client_nameObject


87
88
89
# File 'lib/chef/http/authenticator.rb', line 87

def client_name
  @auth_credentials.client_name
end

#decrypt_pfx_passObject


111
112
113
# File 'lib/chef/http/authenticator.rb', line 111

def decrypt_pfx_pass
  self.class.decrypt_pfx_pass
end

#detect_certificate_key(client_name) ⇒ Object


91
92
93
# File 'lib/chef/http/authenticator.rb', line 91

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

#encrypt_pfx_passObject


107
108
109
# File 'lib/chef/http/authenticator.rb', line 107

def encrypt_pfx_pass
  self.class.encrypt_pfx_pass
end

#get_cert_passwordObject


103
104
105
# File 'lib/chef/http/authenticator.rb', line 103

def get_cert_password
  self.class.get_cert_password
end

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


53
54
55
56
57
# File 'lib/chef/http/authenticator.rb', line 53

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


59
60
61
# File 'lib/chef/http/authenticator.rb', line 59

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


67
68
69
# File 'lib/chef/http/authenticator.rb', line 67

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


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

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


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

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


99
100
101
# File 'lib/chef/http/authenticator.rb', line 99

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

#sign_requests?Boolean

Returns:

  • (Boolean)

83
84
85
# File 'lib/chef/http/authenticator.rb', line 83

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

#stream_response_handler(response) ⇒ Object


63
64
65
# File 'lib/chef/http/authenticator.rb', line 63

def stream_response_handler(response)
  nil
end