class Chef::Resource::WindowsCertificate
Constants
- CERT_SYSTEM_STORE_CURRENT_USER
- CERT_SYSTEM_STORE_LOCAL_MACHINE
Public Instance Methods
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
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
# 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
# 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
# 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
# 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
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
# File lib/chef/resource/windows_certificate.rb, line 250 def is_file?(source) ::File.file?(source) end
# 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
# 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
# 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
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
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
# 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
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
# 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