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 338
        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 184
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 189
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 321
        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 306
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 195
def delete_cert
  store = ::Win32::Certstore.open(new_resource.store_name, store_location: native_cert_location)
  store.delete(validate_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 440
def export_cert(cert_obj, output_path:, store_name:, store_location:, pfx_password:)
  # Delete the cert if it exists on disk already.
  # We want to ensure we're not randomly loading an old stinky cert.
  if ::File.exist?(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"
    validated_thumbprint = validate_thumbprint(new_resource.source)
    if validated_thumbprint != false # is the thumbprint valid
      store = ::Win32::Certstore.open(new_resource.store_name, store_location: native_cert_location)
      result = store.valid?(new_resource.source) # is there a cert in the store matching that thumbprint
      temp = result == ( "Certificate Not Found" || "Certificate Has Expired" ) ? false : true
      if temp == true
        pfx_ps_cmd(validate_thumbprint(new_resource.source), store_location: store_location, store_name: store_name, output_path: output_path, password: pfx_password )
      else
        Chef::Log.debug("The requested certificate is not found or has expired")
      end
    else
      message = "While exporting the pfx, was passed the following invalid certificate thumbprint : #{new_resource.source}\n"
      raise Chef::Exceptions::InvalidKeyAttribute, message
    end
  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 200
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(validate_thumbprint(new_resource.source))
  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 381
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 210
def fetch_key
  require "openssl" unless defined?(OpenSSL)
  file_name = ::File.basename(new_resource.output_path, ::File.extname(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(validate_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 223
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 234
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 259
        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 497
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
    if verify_cert(thumbprint) == true
      Chef::Log.debug("Certificate is already present")
    elsif verify_cert(thumbprint) == false # Not found already in the CertStore
      if is_pfx
        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
      else
        converge_by("Creating a certificate #{new_resource.source} for Store #{new_resource.store_name}") do
          add_cert(cert_obj)
        end
      end
    else
      message = "Certificate could not be imported"
      raise Chef::Exceptions::CertificateNotImportable, message
    end
  end
end
is_file?(source) click to toggle source
# File lib/chef/resource/windows_certificate.rb, line 250
def is_file?(source)
  ::File.file?(source)
end
is_url?(source) click to toggle source
# File lib/chef/resource/windows_certificate.rb, line 244
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 302
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 294
        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 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 290
def ps_cert_location
  new_resource.user_store ? "CurrentUser" : "LocalMachine"
end
valid_thumbprint?(string) click to toggle source

Thumbprints should be exactly 40 Hex characters

# File lib/chef/resource/windows_certificate.rb, line 255
def valid_thumbprint?(string)
  string.match?(/[0-9A-Fa-f]/) && string.length == 40
end
validate_thumbprint(thumbprint) click to toggle source
# File lib/chef/resource/windows_certificate.rb, line 266
def validate_thumbprint(thumbprint)
  # valid_thumbprint can return false under at least 2 conditions:
  # one is that the thumbprint is in fact busted
  # the second is that the thumbprint is valid but belongs to an expired certificate already installed
  results = valid_thumbprint?(thumbprint)
  results == true ? thumbprint : false
end
verify_cert(thumbprint = new_resource.source) click to toggle source

Checks to make sure whether the cert is found or not if it IS found, is it still valid - has it expired?

# File lib/chef/resource/windows_certificate.rb, line 276
def verify_cert(thumbprint = new_resource.source)
  store = ::Win32::Certstore.open(new_resource.store_name, store_location: native_cert_location)
  validated_thumbprint = validate_thumbprint(thumbprint)
  if validated_thumbprint != false
    result = store.valid?(thumbprint)
    result == ( "Certificate Not Found" || "Certificate Has Expired" ) ? false : true
  else
    message = "While verifying the certificate, was passed the following invalid certificate thumbprint : #{thumbprint}\n"
    raise Chef::Exceptions::InvalidKeyAttribute, message
  end
end
within_store_script() { |"$store"| ... } click to toggle source
# File lib/chef/resource/windows_certificate.rb, line 328
        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