class Chef::HTTP::Authenticator

Constants

DEFAULT_SERVER_API_VERSION

Attributes

api_version[R]
attr_names[R]
auth_credentials[R]
raw_key[R]
sign_request[RW]
signing_key_filename[R]
version_class[R]

Public Class Methods

check_certstore_for_key(client_name) click to toggle source
# 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) click to toggle source

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.

# 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) click to toggle source
# 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) click to toggle source
# 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) click to toggle source
# 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) click to toggle source

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

# 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) click to toggle source
# 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_password() click to toggle source
# 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_user() click to toggle source
# 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_user() click to toggle source
# 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) click to toggle source
# 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) click to toggle source
# 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) click to toggle source
# 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
new(opts = {}) click to toggle source
# 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
retrieve_certificate_key(client_name) click to toggle source
# 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

Public Instance Methods

authentication_headers(method, url, json_body = nil, headers = nil) click to toggle source
# 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) click to toggle source
# 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_name() click to toggle source
# File lib/chef/http/authenticator.rb, line 85
def client_name
  @auth_credentials.client_name
end
decrypt_pfx_pass_with_vector() click to toggle source
# 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) click to toggle source
# 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_vector() click to toggle source
# File lib/chef/http/authenticator.rb, line 105
def encrypt_pfx_pass_with_vector
  self.class.encrypt_pfx_pass_with_vector
end
get_cert_password() click to toggle source
# 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) click to toggle source
# 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) click to toggle source
# 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) click to toggle source
# 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) click to toggle source
# 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_version() click to toggle source
# 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) click to toggle source
# File lib/chef/http/authenticator.rb, line 97
def retrieve_certificate_key(client_name)
  self.class.retrieve_certificate_key(client_name)
end
sign_requests?() click to toggle source
# File lib/chef/http/authenticator.rb, line 81
def sign_requests?
  auth_credentials.sign_requests? && @sign_request
end
stream_response_handler(response) click to toggle source
# File lib/chef/http/authenticator.rb, line 61
def stream_response_handler(response)
  nil
end