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 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
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
# 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
# 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
# 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
# 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
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
# File lib/chef/resource/windows_certificate.rb, line 244 def is_file?(source) ::File.file?(source) end
# 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
# 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
# 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
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
# 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
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
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
# 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