class Chef::Resource::WindowsCertificate

Constants

CERT_SYSTEM_STORE_CURRENT_USER
CERT_SYSTEM_STORE_LOCAL_MACHINE

Public Instance Methods

acl_script(hash) click to toggle source
# File lib/chef/resource/windows_certificate.rb, line 336
        def acl_script(hash)
          return "" if new_resource.private_key_acl.nil? || new_resource.private_key_acl.empty?

          # this PS came from http://blogs.technet.com/b/operationsguy/archive/2010/11/29/provide-access-to-private-keys-commandline-vs-powershell.aspx
          # and from https://msdn.microsoft.com/en-us/library/windows/desktop/bb204778(v=vs.85).aspx
          set_acl_script = <<-EOH
  $hash = #{hash}
  $storeCert = Get-ChildItem "cert:\\#{ps_cert_location}\\#{new_resource.store_name}\\$hash"
  if ($storeCert -eq $null) { throw 'no key exists.' }
  $keyname = $storeCert.PrivateKey.CspKeyContainerInfo.UniqueKeyContainerName
  if ($keyname -eq $null) { throw 'no private key exists.' }
  if ($storeCert.PrivateKey.CspKeyContainerInfo.MachineKeyStore)
  {
    $fullpath = "$Env:ProgramData\\Microsoft\\Crypto\\RSA\\MachineKeys\\$keyname"
  }
  else
  {
    $currentUser = New-Object System.Security.Principal.NTAccount($Env:UserDomain, $Env:UserName)
    $userSID = $currentUser.Translate([System.Security.Principal.SecurityIdentifier]).Value
    $fullpath = "$Env:ProgramData\\Microsoft\\Crypto\\RSA\\$userSID\\$keyname"
  }
          EOH
          new_resource.private_key_acl.each do |name|
            set_acl_script << "$uname='#{name}'; icacls $fullpath /grant $uname`:RX\n"
          end
          set_acl_script
        end
add_cert(cert_obj) click to toggle source
# File lib/chef/resource/windows_certificate.rb, line 177
def add_cert(cert_obj)
  store = ::Win32::Certstore.open(new_resource.store_name, store_location: native_cert_location)
  store.add(cert_obj)
end
add_pfx_cert(path) click to toggle source
# File lib/chef/resource/windows_certificate.rb, line 182
def add_pfx_cert(path)
  exportable = new_resource.exportable ? 1 : 0
  store = ::Win32::Certstore.open(new_resource.store_name, store_location: native_cert_location)
  store.add_pfx(path, new_resource.pfx_password, exportable)
end
cert_exists_script(hash) click to toggle source
# File lib/chef/resource/windows_certificate.rb, line 319
        def cert_exists_script(hash)
          <<-EOH
  $hash = #{hash}
  Test-Path "Cert:\\#{ps_cert_location}\\#{new_resource.store_name}\\$hash"
          EOH
        end
cert_script(persist) click to toggle source
# File lib/chef/resource/windows_certificate.rb, line 304
def cert_script(persist)
  cert_script = "$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2"
  file = Chef::Util::PathHelper.cleanpath(new_resource.source, ps_cert_location)
  cert_script << " \"#{file}\""
  if ::File.extname(file.downcase) == ".pfx"
    cert_script << ", \"#{new_resource.pfx_password}\""
    if persist && new_resource.user_store
      cert_script << ", ([System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet)"
    elsif persist
      cert_script << ", ([System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet -bor [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::MachineKeyset)"
    end
  end
  cert_script << "\n"
end
delete_cert() click to toggle source
# File lib/chef/resource/windows_certificate.rb, line 188
def delete_cert
  store = ::Win32::Certstore.open(new_resource.store_name, store_location: native_cert_location)
  store.delete(resolve_thumbprint(new_resource.source))
end
export_cert(cert_obj, output_path:, store_name:, store_location:, pfx_password:) click to toggle source
# File lib/chef/resource/windows_certificate.rb, line 438
def export_cert(cert_obj, output_path:, store_name:, store_location:, pfx_password:)
  # Delete the cert if it exists. This is non-destructive in that it only removes the file and not the entire path.
  # We want to ensure we're not randomly loading an old stinky cert.
  if ::File.exists?(output_path)
    ::File.delete(output_path)
  end

  unless ::File.directory?(::File.dirname(output_path))
    FileUtils.mkdir_p(::File.dirname(output_path))
  end

  out_file = ::File.new(output_path, "w+")

  case ::File.extname(output_path)
  when ".pem"
    out_file.puts(cert_obj)
  when ".der"
    out_file.puts(cert_obj.to_der)
  when ".cer"
    cert_out = shell_out("openssl x509 -text -inform DER -in #{cert_obj.to_pem} -outform CER").stdout
    out_file.puts(cert_out)
  when ".crt"
    cert_out = shell_out("openssl x509 -text -inform DER -in #{cert_obj} -outform CRT").stdout
    out_file.puts(cert_out)
  when ".pfx"
    pfx_ps_cmd(resolve_thumbprint(new_resource.source), store_location: store_location, store_name: store_name, output_path: output_path, password: pfx_password )
  when ".p7b"
    cert_out = shell_out("openssl pkcs7 -export -nokeys -in #{cert_obj.to_pem} -outform P7B").stdout
    out_file.puts(cert_out)
  when ".key"
    out_file.puts(cert_obj)
  else
    Chef::Log.info("Supported certificate format .pem, .der, .cer, .crt, and .p7b")
  end

  out_file.close
end
fetch_cert() click to toggle source
# File lib/chef/resource/windows_certificate.rb, line 193
def fetch_cert
  store = ::Win32::Certstore.open(new_resource.store_name, store_location: native_cert_location)
  if new_resource.output_path && ::File.extname(new_resource.output_path) == ".key"
    fetch_key

  else
    store.get(resolve_thumbprint(new_resource.source), store_name: new_resource.store_name, store_location: native_cert_location)
  end
end
fetch_cert_object_from_file(ext) click to toggle source

Method returns an OpenSSL::X509::Certificate object. Might also return multiple certificates if present in certificate path

Based on its extension, the certificate contents are used to initialize PKCS12 (PFX), PKCS7 (P7B) objects which contains OpenSSL::X509::Certificate.

@note Other then PEM, all the certificates are usually in binary format, and hence

their contents are loaded by using File.binread

@param ext [String] Extension of the certificate

@return [OpenSSL::X509::Certificate] Object containing certificate's attributes

@raise [OpenSSL::PKCS12::PKCS12Error] When incorrect password is provided for PFX certificate

# File lib/chef/resource/windows_certificate.rb, line 379
def fetch_cert_object_from_file(ext)
  if is_file?(new_resource.source)
    begin
      ::File.exist?(new_resource.source)
      contents = ::File.binread(new_resource.source)
    rescue => exception
      message = "Unable to load the certificate object from the specified local path : #{new_resource.source}\n"
      message << exception.message
      raise Chef::Exceptions::FileNotFound, message
    end
  elsif is_url?(new_resource.source)
    require "uri" unless defined?(URI)
    uri = URI(new_resource.source)
    state = uri.is_a?(URI::HTTP) && !uri.host.nil? ? true : false
    if state
      begin
        output_file_name = get_file_name(new_resource.source)
        unless Dir.exist?(Chef::Config[:file_cache_path])
          Dir.mkdir(Chef::Config[:file_cache_path])
        end
        local_path = ::File.join(Chef::Config[:file_cache_path], output_file_name)
        @local_pfx_path = local_path
        ::File.open(local_path, "wb") do |file|
          file.write URI.open(new_resource.source).read
        end
      rescue => exception
        message = "Not Able to Download Certificate Object at the URL specified : #{new_resource.source}\n"
        message << exception.message
        raise Chef::Exceptions::FileNotFound, message
      end

      contents = ::File.binread(local_path)

    else
      message = "Not Able to Download Certificate Object at the URL specified : #{new_resource.source}\n"
      message << exception.message
      raise Chef::Exceptions::InvalidRemoteFileURI, message
    end
  else
    message = "You passed an invalid file or url to import. Please check the spelling and try again."
    message << exception.message
    raise Chef::Exceptions::ArgumentError, message
  end

  case ext
  when ".pfx"
    pfx = OpenSSL::PKCS12.new(contents, new_resource.pfx_password)
    if pfx.ca_certs.nil?
      pfx.certificate
    else
      [pfx.certificate] + pfx.ca_certs
    end
  when ".p7b"
    OpenSSL::PKCS7.new(contents).certificates
  else
    OpenSSL::X509::Certificate.new(contents)
  end
end
fetch_key() click to toggle source
# File lib/chef/resource/windows_certificate.rb, line 203
def fetch_key
  require "openssl" unless defined?(OpenSSL)
  file_name = ::File.basename(new_resource.output_path, ::File.extname(new_resource.output_path))
  directory = ::File.dirname(new_resource.output_path)
  pfx_file = file_name + ".pfx"
  new_pfx_output_path = ::File.join(Chef::FileCache.create_cache_path("pfx_files"), pfx_file)
  powershell_exec(pfx_ps_cmd(resolve_thumbprint(new_resource.source), store_location: ps_cert_location, store_name: new_resource.store_name, output_path: new_pfx_output_path, password: new_resource.pfx_password ))
  pkcs12 = OpenSSL::PKCS12.new(::File.binread(new_pfx_output_path), new_resource.pfx_password)
  f = ::File.open(new_resource.output_path, "w")
  f.write(pkcs12.key.to_s)
  f.flush
  f.close
end
get_file_extension(file_name) click to toggle source
# File lib/chef/resource/windows_certificate.rb, line 217
def get_file_extension(file_name)
  if is_file?(file_name)
    ::File.extname(file_name)
  elsif is_url?(file_name)
    require "open-uri" unless defined?(OpenURI)
    uri = URI.parse(file_name)
    output_file = ::File.basename(uri.path)
    ::File.extname(output_file)
  end
end
get_file_name(path_name) click to toggle source
# File lib/chef/resource/windows_certificate.rb, line 228
def get_file_name(path_name)
  if is_file?(path_name)
    ::File.extname(path_name)
  elsif is_url?(path_name)
    require "open-uri" unless defined?(OpenURI)
    uri = URI.parse(path_name)
    ::File.basename(uri.path)
  end
end
get_thumbprint(store_name, location, source) click to toggle source
# File lib/chef/resource/windows_certificate.rb, line 257
        def get_thumbprint(store_name, location, source)
          <<-GETTHUMBPRINTCODE
            $content = Get-ChildItem  -Path Cert:\\#{location}\\#{store_name} | Where-Object {$_.Subject -Match "#{source}"} | Select-Object Thumbprint
            $content.thumbprint
          GETTHUMBPRINTCODE
        end
import_certificates(cert_objs, is_pfx, store_name: new_resource.store_name, store_location: native_cert_location) click to toggle source

Imports the certificate object into cert store

@param cert_objs [OpenSSL::X509::Certificate] Object containing certificate's attributes

@param is_pfx [Boolean] true if we want to import a PFX certificate

# File lib/chef/resource/windows_certificate.rb, line 482
def import_certificates(cert_objs, is_pfx, store_name: new_resource.store_name, store_location: native_cert_location)
  [cert_objs].flatten.each do |cert_obj|
    # thumbprint = OpenSSL::Digest.new("SHA1", cert_obj.to_der).to_s
    # pkcs = OpenSSL::PKCS12.new(cert_obj, new_resource.pfx_password)
    # cert = OpenSSL::X509::Certificate.new(pkcs.certificate.to_pem)
    thumbprint = OpenSSL::Digest.new("SHA1", cert_obj.to_der).to_s
    if is_pfx
      if verify_cert(thumbprint) == true
        Chef::Log.debug("Certificate is already present")
      else
        if is_file?(new_resource.source)
          converge_by("Creating a PFX #{new_resource.source} for Store #{new_resource.store_name}") do
            add_pfx_cert(new_resource.source)
          end
        elsif is_url?(new_resource.source)
          converge_by("Creating a PFX #{@local_pfx_path} for Store #{new_resource.store_name}") do
            add_pfx_cert(@local_pfx_path)
          end
        else
          message = "You passed an invalid file or url to import. Please check the spelling and try again."
          message << exception.message
          raise Chef::Exceptions::ArgumentError, message
        end
      end
    else
      if verify_cert(thumbprint) == true
        Chef::Log.debug("Certificate is already present")
      else
        converge_by("Creating a certificate #{new_resource.source} for Store #{new_resource.store_name}") do
          add_cert(cert_obj)
        end
      end
    end
  end
end
is_file?(source) click to toggle source
# File lib/chef/resource/windows_certificate.rb, line 244
def is_file?(source)
  ::File.file?(source)
end
is_url?(source) click to toggle source
# File lib/chef/resource/windows_certificate.rb, line 238
def is_url?(source)
  require "uri" unless defined?(URI)
  uri = URI.parse(source)
  uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
end
native_cert_location() click to toggle source
# File lib/chef/resource/windows_certificate.rb, line 300
def native_cert_location
  new_resource.user_store ? CERT_SYSTEM_STORE_CURRENT_USER : CERT_SYSTEM_STORE_LOCAL_MACHINE
end
pfx_ps_cmd(thumbprint, store_location: "LocalMachine", store_name: "My", output_path:, password: ) click to toggle source
# File lib/chef/resource/windows_certificate.rb, line 292
        def pfx_ps_cmd(thumbprint, store_location: "LocalMachine", store_name: "My", output_path:, password: )
          <<-CMD
            $my_pwd = ConvertTo-SecureString -String "#{password}" -Force -AsPlainText
            $cert = Get-ChildItem -path cert:\\#{store_location}\\#{store_name} -Recurse | Where { $_.Thumbprint -eq "#{thumbprint.upcase}" }
            Export-PfxCertificate -Cert $cert -FilePath "#{output_path}" -Password $my_pwd
          CMD
        end
ps_cert_location() click to toggle source

this array structure is solving 2 problems. The first is that we need to have support for both the CurrentUser AND LocalMachine stores Secondly, we need to pass the proper constant name for each store to win32-certstore but also pass the short name to powershell scripts used here

# File lib/chef/resource/windows_certificate.rb, line 288
def ps_cert_location
  new_resource.user_store ? "CurrentUser" : "LocalMachine"
end
resolve_thumbprint(thumbprint) click to toggle source
# File lib/chef/resource/windows_certificate.rb, line 264
def resolve_thumbprint(thumbprint)
  return thumbprint if valid_thumbprint?(thumbprint)

  powershell_exec!(get_thumbprint(new_resource.store_name, ps_cert_location, new_resource.source)).result
end
valid_thumbprint?(string) click to toggle source

Thumbprints should be exactly 40 Hex characters

# File lib/chef/resource/windows_certificate.rb, line 253
def valid_thumbprint?(string)
  string.match?(/[0-9A-Fa-f]/) && string.length == 40
end
verify_cert(thumbprint = new_resource.source) click to toggle source

Checks whether a certificate with the given thumbprint is already present and valid in certificate store If the certificate is not present, verify_cert returns a String: “Certificate not found” But if it is present but expired, it returns a Boolean: false Otherwise, it returns a Boolean: true updated this method to accept either a subject name or a thumbprint - 1/29/2021

# File lib/chef/resource/windows_certificate.rb, line 277
def verify_cert(thumbprint = new_resource.source)
  store = ::Win32::Certstore.open(new_resource.store_name, store_location: native_cert_location)
  if new_resource.pfx_password.nil?
    store.valid?(resolve_thumbprint(thumbprint), store_location: native_cert_location, store_name: new_resource.store_name )
  else
    store.valid?(resolve_thumbprint(thumbprint), store_location: native_cert_location, store_name: new_resource.store_name)
  end
end
within_store_script() { |"$store"| ... } click to toggle source
# File lib/chef/resource/windows_certificate.rb, line 326
        def within_store_script
          inner_script = yield "$store"
          <<-EOH
  $store = New-Object System.Security.Cryptography.X509Certificates.X509Store "#{new_resource.store_name}", ([System.Security.Cryptography.X509Certificates.StoreLocation]::#{ps_cert_location})
  $store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite)
  #{inner_script}
  $store.Close()
          EOH
        end