diff --git a/.expeditor/verify.pipeline.yml b/.expeditor/verify.pipeline.yml index 862cf2c..5c23013 100644 --- a/.expeditor/verify.pipeline.yml +++ b/.expeditor/verify.pipeline.yml @@ -17,14 +17,6 @@ steps: docker: image: ruby:2.7-buster -- label: run-specs-ruby-2.5 - command: - - .expeditor/run_linux_tests.sh rspec - expeditor: - executor: - docker: - image: ruby:2.5-buster - - label: run-specs-ruby-2.6 command: - .expeditor/run_linux_tests.sh rspec @@ -50,11 +42,12 @@ steps: image: ruby:3.0-buster - label: "run specs :windows:" - command: - - bundle config set --local without docs debug - - bundle install --jobs=7 --retry=3 - - bundle exec rake spec + commands: + # - Get-Childitem .expeditor + - .expeditor/verify_win32certstore.ps1 expeditor: executor: docker: host_os: windows + image: rubydistros/windows-2019:3.0 + shell: [ "powershell", "-Command" ] diff --git a/.expeditor/verify_win32certstore.ps1 b/.expeditor/verify_win32certstore.ps1 new file mode 100644 index 0000000..7f8ef72 --- /dev/null +++ b/.expeditor/verify_win32certstore.ps1 @@ -0,0 +1,86 @@ +#!/usr/bin/env powershell + +#Requires -Version 5 + + +$ErrorActionPreference = "Stop" + +Write-Output "--- :ruby: Removing existing Ruby instances" + +$rubies = Get-ChildItem -Path "C:\ruby*" +foreach ($ruby in $rubies){ + Remove-Item -LiteralPath $ruby.FullName -Recurse -Force -ErrorAction SilentlyContinue +} +Write-Output "`r" + +Write-Output "--- :screwdriver: Installing the latest Chef-Client" +choco install chef-client -y +if (-not $?) { throw "unable to install Chef-Client" } +Write-Output "`r" + +Write-Output "--- :chopsticks: Refreshing the build environment to pick up Chef binaries" +refreshenv +$env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User") + ";c:\opscode\chef\embedded\bin" +Write-Output "`r" + +Write-Output "--- :building_construction: Correcting a gem build problem, moving header files around" +$filename = "ansidecl.h" +$locale = Get-ChildItem -path c:\opscode -Include $filename -Recurse -ErrorAction Ignore +Write-Output "Copying ansidecl.h to the correct folder" +$parent_folder = $locale.Directory.Parent.FullName +$child_folder = $parent_folder + "\x86_64-w64-mingw32\include" +Copy-Item $locale.FullName -Destination $child_folder -ErrorAction Continue +Write-Output "`r" + +Write-Output "--- :bank: Installing Gems for the Chef-PowerShell Gem" +gem install bundler:2.2.29 +gem install libyajl2-gem +gem install chef-powershell +if (-not $?) { throw "unable to install this build"} +Write-Output "`r" + +Write-Output "--- :bank: Installing Node via Choco" +choco install nodejs -y +if (-not $?) { throw "unable to install Node"} +Write-Output "`r" + +Write-Output "--- :bank: Refreshing the build environment to pick up Node.js binaries" +refreshenv +$env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User") + ";c:\opscode\chef\embedded\bin" +Write-Output "`r" + +Write-Output "--- :bank: Installing CSPell via NPM, Getting Ready to SpellCheck the Gem code" +npm install -g cspell +if (-not $?) { throw "unable to install CSpell"} +Write-Output "`r" + +Write-Output "--- :building_construction: Setting up Environment Variables for Ruby and Chef PowerShell" +$temp = Get-Location +$gem_path = [string]$temp.path + "\vendor\bundle\ruby\3.0.0" +[Environment]::SetEnvironmentVariable("GEM_PATH", $gem_path) +[Environment]::SetEnvironmentVariable("GEM_ROOT", $gem_path) +[Environment]::SetEnvironmentVariable("BUNDLE_GEMFILE", "$($temp.path)\Gemfile") +Write-Output "`r" + +Write-Output "--- :put_litter_in_its_place: Removing any existing Chef PowerShell DLL's since they'll conflict with rspec" +# remove the existing chef.powershell.dll and chef.powershell.wrapper.dll files under embedded\bin +$file = get-command bundle +$parent_folder = Split-Path -Path $file.Source +Write-Output "Removing files from here : $parent_folder" +if (Test-Path $($parent_folder + "\chef.powershell.dll")){ + Remove-item -path $($parent_folder + "\chef.powershell.dll") + Remove-item -path $($parent_folder + "\chef.powershell.wrapper.dll") +} +Write-Output "`r" + +Write-Output "--- :mag: Where are all the Chef PowerShell DLLs located?" +$files = Get-ChildItem -Path c:\ -Name "Chef.PowerShell.Wrapper.dll" -Recurse +foreach($file in $files){ + Write-Output "I found a copy here: $file" +} + +Write-Output "--- :point_right: finally verifying the gem code" +bundle update +bundle exec rake spec +if (-not $?) { throw "Bundle Gem failed"} +Write-Output "`r" diff --git a/Gemfile b/Gemfile index ac310c2..500f8d0 100644 --- a/Gemfile +++ b/Gemfile @@ -3,8 +3,6 @@ source "https://rubygems.org" # Specify your gem's dependencies in win32-certstore.gemspec gemspec -gem "mixlib-shellout", "< 3.2.3" - if Gem.ruby_version.to_s.start_with?("2.5") # 16.7.23 required ruby 2.6+ gem "chef-utils", "< 16.7.23" # TODO: remove when we drop ruby 2.5 diff --git a/lib/win32/certstore.rb b/lib/win32/certstore.rb index 326e4eb..cacc42a 100644 --- a/lib/win32/certstore.rb +++ b/lib/win32/certstore.rb @@ -29,23 +29,23 @@ class Certstore include Win32::Certstore::Mixin::String include Win32::Certstore::StoreBase - attr_accessor :store_name + attr_accessor :store_name, :store_location # Initializes a new instance of a certificate store. # takes 2 parameters - the store name (My, Root, etc) and the location (CurrentUser or LocalMachine), it defaults to LocalMachine for backwards compatibility - def initialize(store_name, store_location: CERT_SYSTEM_STORE_LOCAL_MACHINE) + def initialize(store_name, store_location) @store_name = store_name @store_location = store_location - @certstore_handler = open(store_name, store_location: store_location) + @certstore_handler = open(store_name, store_location) end # To open given certificate store def self.open(store_name, store_location: CERT_SYSTEM_STORE_LOCAL_MACHINE) validate_store(store_name) if block_given? - yield new(store_name, store_location: store_location) + yield new(store_name, store_location) else - new(store_name, store_location: store_location) + new(store_name, store_location) end end @@ -74,15 +74,15 @@ def add_pfx(path, password, key_properties = 0) # Return `OpenSSL::X509` certificate object # @param request [thumbprint] of certificate # @return [Object] of certificates in OpenSSL::X509 format - def get(certificate_thumbprint, store_name: @store_name, store_location: @store_location) - cert_get(certificate_thumbprint, store_name: store_name, store_location: store_location) + def get(certificate_thumbprint) + cert_get(certificate_thumbprint) end # Return `OpenSSL::X509` certificate object if present otherwise raise a "Certificate not found!" error # @param request [thumbprint] of certificate # @return [Object] of certificates in OpenSSL::X509 format - def get!(certificate_thumbprint, store_name: @store_name, store_location: @store_location) - cert_pem = cert_get(certificate_thumbprint, store_name: store_name, store_location: store_location) + def get!(certificate_thumbprint) + cert_pem = cert_get(certificate_thumbprint) raise ArgumentError, "Unable to retrieve the certificate" if cert_pem.empty? @@ -110,11 +110,15 @@ def search(search_token) cert_search(certstore_handler, search_token) end + def get_thumbprint(search_token) + cert_lookup_by_token(search_token) + end + # Validates a certificate in a certificate store on the basis of time validity # @param request[thumbprint] of certificate # @return [true, false] only true or false - def valid?(certificate_thumbprint, store_location: "", store_name: "") - cert_validate(certificate_thumbprint, store_location: store_location, store_name: store_name).yield_self do |x| + def valid?(certificate_thumbprint) + cert_validate(certificate_thumbprint).yield_self do |x| if x.is_a?(TrueClass) || x.is_a?(FalseClass) x else @@ -139,7 +143,7 @@ def self.finalize(certstore_handler) # To open certstore and return open certificate store pointer - def open(store_name, store_location: CERT_SYSTEM_STORE_LOCAL_MACHINE) + def open(store_name, store_location) certstore_handler = CertOpenStore(CERT_STORE_PROV_SYSTEM, 0, nil, store_location, wstring(store_name)) unless certstore_handler last_error = FFI::LastError.error diff --git a/lib/win32/certstore/mixin/assertions.rb b/lib/win32/certstore/mixin/assertions.rb index 7f343a5..a7f1936 100644 --- a/lib/win32/certstore/mixin/assertions.rb +++ b/lib/win32/certstore/mixin/assertions.rb @@ -37,13 +37,13 @@ def validate_certificate(cert_file_path) # Validate certificate Object def validate_certificate_obj(cert_obj) unless cert_obj.class == OpenSSL::X509::Certificate - raise ArgumentError, "Invalid Certificate object." + raise ArgumentError, "Invalid Certificate object. This is not a properly formatted x509 object" end end # Validate thumbprint def validate_thumbprint(cert_thumbprint) - if cert_thumbprint.nil? || cert_thumbprint.strip.empty? + if cert_thumbprint.nil? || cert_thumbprint.empty? || cert_thumbprint.strip.empty? raise ArgumentError, "Invalid certificate thumbprint." end end diff --git a/lib/win32/certstore/mixin/helper.rb b/lib/win32/certstore/mixin/helper.rb index 8ebcb60..44ac440 100644 --- a/lib/win32/certstore/mixin/helper.rb +++ b/lib/win32/certstore/mixin/helper.rb @@ -22,17 +22,20 @@ class Certstore module Mixin module Helper def cert_ps_cmd(thumbprint, store_location: "LocalMachine", store_name: "My") + # the PowerShell block below uses a "Here-String" - it is explicitly formatted against the left margin. <<-EOH - $cert = Get-ChildItem Cert:\\#{store_location}\\#{store_name} -Recurse | Where { $_.Thumbprint -eq "#{thumbprint}" } + $cert = Get-ChildItem Cert:\\#{store_location}\\#{store_name} -Recurse | Where-Object { $_.Thumbprint -eq "#{thumbprint}" } + $certdata = [System.Convert]::ToBase64String($cert.RawData, 'InsertLineBreaks') $content = $null if($null -ne $cert) { - $content = @( - '-----BEGIN CERTIFICATE-----' - [System.Convert]::ToBase64String($cert.RawData, 'InsertLineBreaks') - '-----END CERTIFICATE-----' - ) + $content = +@" +-----BEGIN CERTIFICATE----- +$($certdata) +-----END CERTIFICATE----- +"@ } $content EOH diff --git a/lib/win32/certstore/mixin/shell_exec.rb b/lib/win32/certstore/mixin/shell_exec.rb deleted file mode 100644 index 8021924..0000000 --- a/lib/win32/certstore/mixin/shell_exec.rb +++ /dev/null @@ -1,105 +0,0 @@ -# -# Author:: Daniel DeLeo () -# Copyright:: Copyright (c) 2017 Chef Software, Inc. -# License:: Apache License, Version 2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -require "mixlib/shellout" unless defined?(Mixlib::ShellOut) - -module Win32 - class Certstore - module Mixin - module ShellExec - def shell_out_command(*command_args) - cmd = Mixlib::ShellOut.new(*command_args) - cmd.live_stream - cmd.run_command - if cmd.error! - raise Mixlib::ShellOut::ShellCommandFailed, cmd.error! - end - - cmd - end - - # Run a command under powershell with the same API as shell_out. The - # options hash is extended to take an "architecture" flag which - # can be set to :i386 or :x86_64 to force the windows architecture. - # - # @param script [String] script to run - # @param options [Hash] options hash - # @return [Mixlib::Shellout] mixlib-shellout object - def powershell_exec(*command_args) - script = command_args.first - options = command_args.last.is_a?(Hash) ? command_args.last : nil - - run_command_with_os_architecture(script, options) - end - - # Run a command under powershell with the same API as shell_out! - # (raises exceptions on errors) - # - # @param script [String] script to run - # @param options [Hash] options hash - # @return [Mixlib::Shellout] mixlib-shellout object - def powershell_exec!(*command_args) - cmd = powershell_exec(*command_args) - cmd.error! - cmd - end - - private - - # Helper function to run shell_out and wrap it with the correct - # flags to possibly disable WOW64 redirection (which we often need - # because chef-client runs as a 32-bit app on 64-bit windows). - # - # @param script [String] script to run - # @param options [Hash] options hash - # @return [Mixlib::Shellout] mixlib-shellout object - def run_command_with_os_architecture(script, options) - options ||= {} - options = options.dup - - shell_out_command( - build_powershell_command(script), - options - ) - end - - # Helper to build a powershell command around the script to run. - # - # @param script [String] script to run - # @return [String] powershell command to execute - def build_powershell_command(script) - flags = [ - # Hides the copyright banner at startup. - "-NoLogo", - # Does not present an interactive prompt to the user. - "-NonInteractive", - # Does not load the Windows PowerShell profile. - "-NoProfile", - # always set the ExecutionPolicy flag - # see http://technet.microsoft.com/en-us/library/ee176961.aspx - "-ExecutionPolicy Unrestricted", - # Powershell will hang if STDIN is redirected - # http://connect.microsoft.com/PowerShell/feedback/details/572313/powershell-exe-can-hang-if-stdin-is-redirected - "-InputFormat None", - ] - - "powershell.exe #{flags.join(" ")} -Command \"#{script.gsub('"', '\"')}\"" - end - end - end - end -end diff --git a/lib/win32/certstore/store_base.rb b/lib/win32/certstore/store_base.rb index c7407ca..3ed2c2f 100644 --- a/lib/win32/certstore/store_base.rb +++ b/lib/win32/certstore/store_base.rb @@ -17,20 +17,25 @@ require_relative "mixin/crypto" require_relative "mixin/string" -require_relative "mixin/shell_exec" require_relative "mixin/unicode" require "openssl" unless defined?(OpenSSL) require "json" unless defined?(JSON) +begin + require "chef-powershell" +rescue LoadError + puts "Not loading powershell_exec during testing" +end + module Win32 class Certstore module StoreBase include Win32::Certstore::Mixin::Crypto include Win32::Certstore::Mixin::Assertions include Win32::Certstore::Mixin::String - include Win32::Certstore::Mixin::ShellExec include Win32::Certstore::Mixin::Unicode include Win32::Certstore::Mixin::Helper + include Chef_PowerShell::ChefPowerShell::PowerShellExec # Adding new certification in open certificate and return boolean # store_handler => Open certificate store handler @@ -87,13 +92,13 @@ def cert_add_pfx(certstore_handler, path, password = "", key_properties = 0) # Get certificate from open certificate store and return certificate object # certificate_thumbprint => thumbprint is a hash. which could be sha1 or md5. - def cert_get(certificate_thumbprint, store_name:, store_location:) + def cert_get(certificate_thumbprint) validate_thumbprint(certificate_thumbprint) thumbprint = update_thumbprint(certificate_thumbprint) - cert_pem = get_cert_pem(thumbprint, store_name: store_name, store_location: store_location) + cert_pem = get_cert_pem(thumbprint) cert_pem = format_pem(cert_pem) - - cert_pem.empty? ? cert_pem : build_openssl_obj(cert_pem) + verify_certificate(cert_pem) + cert_pem end # Listing certificate of open certstore and return list in json @@ -120,6 +125,7 @@ def cert_list(store_handler) def cert_delete(store_handler, certificate_thumbprint) validate_thumbprint(certificate_thumbprint) thumbprint = update_thumbprint(certificate_thumbprint) + cert_delete_flag = false begin cert_args = cert_find_args(store_handler, thumbprint) @@ -137,13 +143,12 @@ def cert_delete(store_handler, certificate_thumbprint) # Verify certificate from open certificate store and return boolean or exceptions # store_handler => Open certificate store handler # certificate_thumbprint => thumbprint is a hash. which could be sha1 or md5. - def cert_validate(certificate_thumbprint, store_location:, store_name:) + def cert_validate(certificate_thumbprint) validate_thumbprint(certificate_thumbprint) thumbprint = update_thumbprint(certificate_thumbprint) - cert_pem = get_cert_pem(thumbprint, store_name: store_name, store_location: store_location) - cert_pem = format_pem(cert_pem) - return "Certificate not found" if cert_pem.empty? + cert_pem = get_cert_pem(thumbprint) + cert_pem = format_pem(cert_pem) verify_certificate(cert_pem) end @@ -168,6 +173,26 @@ def cert_search(store_handler, search_token) certificate_list end + # how can I find a cert if I don't have the thumbprint? + def cert_lookup_by_token(search_token, store_name: @store_name, store_location: @store_location, timeout: -1) + raise ArgumentError, "Invalid search token" if !search_token || search_token.strip.empty? + + converted_store = if store_location == CERT_SYSTEM_STORE_LOCAL_MACHINE || store_location == 131072 + "LocalMachine" + else + "CurrentUser" + end + powershell_cmd = <<~EOH + $result = Get-ChildItem -Path Cert:\\#{converted_store}\\#{store_name} | Where-Object { $_.Subject -match "#{search_token.strip}" } | Select-Object Thumbprint + return $result[0].Thumbprint + EOH + + powershell_exec!(powershell_cmd, :powershell, timeout: timeout).result + + rescue Chef_PowerShell::PowerShellExceptions::PowerShellCommandFailed + raise ArgumentError, "Certificate not found while looking for certificate : #{search_token} in store : #{store_name} at this location : #{store_location}" + end + # To close and destroy pointer of open certificate store handler def close_cert_store(certstore_handler = @certstore_handler) closed = CertCloseStore(certstore_handler, CERT_CLOSE_STORE_FORCE_FLAG) @@ -191,6 +216,11 @@ def add_certcontxt_args(certstore_handler, cert_context) [certstore_handler, cert_context, CERT_STORE_ADD_REPLACE_EXISTING, nil] end + # Remove extra space and : from thumbprint + def update_thumbprint(certificate_thumbprint) + certificate_thumbprint.gsub(/[^A-Za-z0-9]/, "") + end + # Match certificate CN exist in cert_rdn def is_cn_match?(cert_rdn, certificate_name) cert_rdn.read_wstring.match(/(^|\W)#{certificate_name}($|\W)/i) @@ -213,13 +243,10 @@ def cert_get_name_args(pcert_context, cert_name, search_type) [pcert_context, search_type, CERT_NAME_ISSUER_FLAG, nil, cert_name, 1024] end - # Remove extra space and : from thumbprint - def update_thumbprint(certificate_thumbprint) - certificate_thumbprint.gsub(/[^A-Za-z0-9]/, "") - end - # Verify OpenSSL::X509::Certificate object def verify_certificate(cert_pem) + raise ArgumentError, "Certificate not found" if cert_pem.empty? + valid_duration?(build_openssl_obj(cert_pem)) end @@ -229,14 +256,16 @@ def der_cert(cert_obj) end # Get certificate pem - def get_cert_pem(thumbprint, store_name:, store_location:) - converted_store = if store_location == CERT_SYSTEM_STORE_LOCAL_MACHINE + def get_cert_pem(thumbprint, store_name: @store_name, store_location: @store_location, timeout: -1) + converted_store = if store_location == CERT_SYSTEM_STORE_LOCAL_MACHINE || store_location == 131072 "LocalMachine" else "CurrentUser" end - get_data = powershell_exec!(cert_ps_cmd(thumbprint, store_location: converted_store, store_name: store_name)) - get_data.stdout + get_data = powershell_exec!(cert_ps_cmd(thumbprint, store_location: converted_store, store_name: store_name), :powershell, timeout: timeout) + get_data.result + rescue Chef_PowerShell::PowerShellExceptions::PowerShellCommandFailed + raise ArgumentError, "PowerShell threw an error retreiving the certificate. You asked for a cert with this thumbprint : #{thumbprint}, located in this store : #{store_name}, at this location : #{store_location}" end # Format pem diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index b4ec59b..9ce8be3 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -5,6 +5,7 @@ def windows? end require "win32-certstore" if windows? +require "chef-powershell" if windows? RSpec.configure do |config| config.filter_run_excluding windows_only: true unless windows? diff --git a/spec/win32/functional/win32/certstore_spec.rb b/spec/win32/functional/win32/certstore_spec.rb index a41a94a..92503f4 100644 --- a/spec/win32/functional/win32/certstore_spec.rb +++ b/spec/win32/functional/win32/certstore_spec.rb @@ -19,9 +19,19 @@ require "spec_helper" require "openssl" unless defined?(OpenSSL) +# CERT_SYSTEM_STORE_LOCAL_MACHINE = 0x00020000 +# CERT_SYSTEM_STORE_CURRENT_USER = 0x00010000 +# X509_ASN_ENCODING = 0x00000001 + # Testing loading certs into LocalMachine - this is testing legacy usage RSpec.describe Win32::Certstore, :windows_only do - before { open_cert_store("My") } + let(:store_location) { CERT_SYSTEM_STORE_CURRENT_USER } + let(:certstore) { Win32::Certstore } + let(:certbase) { Win32::Certstore::StoreBase } + let(:store_name) { "MY" } + before do + @store = certstore.open(store_name, store_location: store_location) + end after(:each) do delete_cert close_store @@ -30,44 +40,43 @@ describe "#get" do before { add_cert } let(:cert_pem) { File.read('.\spec\win32\assets\GlobalSignRootCA.pem') } - # passing valid thumbprint it "returns the certificate_object if found" do thumbprint = "b1bc968bd4f49d622aa89a81f2150152a41d829c" - expect(@store).to receive(:cert_get).with(thumbprint, store_location: CERT_SYSTEM_STORE_CURRENT_USER, store_name: "My").and_return(cert_pem) - @store.get(thumbprint, store_location: CERT_SYSTEM_STORE_CURRENT_USER, store_name: "My") + expect(@store).to receive(:cert_get).with(thumbprint).and_return(cert_pem) + @store.get(thumbprint) end # passing invalid thumbprint - it "returns nil if certificate not found" do - thumbprint = "b1bc968bd4f49d622aa89a81f2150152a41d829cab" - cert_obj = @store.get(thumbprint, store_location: CERT_SYSTEM_STORE_CURRENT_USER, store_name: "My") - expect(cert_obj).to be_empty + it "returns raises an Arugment error" do + thumbprint14 = "b1bc968bd4f49d622aa89a81f2150" + expect { @store.get(thumbprint14) }.to raise_error(ArgumentError) end end describe "#get!" do before { add_cert } let(:cert_pem) { File.read('.\spec\win32\assets\GlobalSignRootCA.pem') } - # passing valid thumbprint it "returns the certificate_object if found" do thumbprint = "b1bc968bd4f49d622aa89a81f2150152a41d829c" - expect(@store).to receive(:cert_get).with(thumbprint, store_location: CERT_SYSTEM_STORE_CURRENT_USER, store_name: "My").and_return(cert_pem) - @store.get!(thumbprint, store_location: CERT_SYSTEM_STORE_CURRENT_USER, store_name: "My") + expect(@store).to receive(:cert_get).with(thumbprint).and_return("foo") + @store.get!(thumbprint) end # passing invalid thumbprint it "returns ArgumentError if certificate not found" do thumbprint = "b1bc968bd4f49d622aa89a81f2150152a41d829cab" - expect { @store.get!(thumbprint, store_location: CERT_SYSTEM_STORE_CURRENT_USER, store_name: "My") }.to raise_error(ArgumentError) + # expect { @store.get!(thumbprint) }.to raise_error(ArgumentError) + expect(@store).to receive(:cert_get).with(thumbprint).and_return(cert_pem) + @store.get!(thumbprint) end end private - def open_cert_store(store) - @store = Win32::Certstore.open(store) + def open_cert_store(store, store_location) + @store = Win32::Certstore.open(store, store_location: store_location) end def add_cert @@ -92,10 +101,14 @@ def delete_cert # Now testing new code and what happens if you want to import something into CurrentUser - 1/29/2021 # Defining the store constant here as the spec doesn't read from the values in the crypto.rb file. -CERT_SYSTEM_STORE_CURRENT_USER = 0x00010000 RSpec.describe Win32::Certstore, :windows_only do - before { open_cert_store("My", CERT_SYSTEM_STORE_CURRENT_USER) } + let(:store_location) { CERT_SYSTEM_STORE_CURRENT_USER } + let(:certstore) { Win32::Certstore } + let(:store_name) { "MY" } + before do + @store = certstore.open(store_name, store_location: store_location) + end after(:each) do delete_cert close_store @@ -108,15 +121,15 @@ def delete_cert # passing valid thumbprint it "returns the certificate_object if found" do thumbprint = "b1bc968bd4f49d622aa89a81f2150152a41d829c" - expect(@store).to receive(:cert_get).with(thumbprint, store_location: CERT_SYSTEM_STORE_CURRENT_USER, store_name: "My").and_return(cert_pem) - @store.get(thumbprint, store_location: CERT_SYSTEM_STORE_CURRENT_USER, store_name: "My") + expect(@store).to receive(:cert_get).with(thumbprint).and_return(cert_pem) + @store.get(thumbprint) end # passing invalid thumbprint it "returns nil if certificate not found" do thumbprint = "b1bc968bd4f49d622aa89a81f2150152a41d829cab" - expect(@store).to receive(:cert_get).with(thumbprint, store_location: CERT_SYSTEM_STORE_CURRENT_USER, store_name: "My").and_return(nil) - @store.get(thumbprint, store_location: CERT_SYSTEM_STORE_CURRENT_USER, store_name: "My") + expect(@store).to receive(:cert_get).with(thumbprint).and_return(nil) + @store.get(thumbprint) end end diff --git a/spec/win32/unit/certstore_spec.rb b/spec/win32/unit/certstore_spec.rb index a090fa2..988419d 100644 --- a/spec/win32/unit/certstore_spec.rb +++ b/spec/win32/unit/certstore_spec.rb @@ -16,34 +16,43 @@ # limitations under the License. # +# Notes : 10/5/2021 - the tests below mock calls to methods in store_base.rb that are powershell_exec calls. This was done because at this point, these calls will resturn an ffi_lib error when running the tests. This is because powershell_exec is part of chef client and not +# a stand-alone gem so when rspec calls into the method there is no way to invoke it. The store_base code works in production because it will always be called under the context of the chef-client. Once the powershell_exec code is moved to the chef-utils gem, a gem dependency +# can be added here and the mocks can be removed in favor of the actual calls. + require "spec_helper" require "openssl" unless defined?(OpenSSL) +require "pry" -describe Win32::Certstore, :windows_only do +CERT_SYSTEM_STORE_LOCAL_MACHINE = 0x00020000 +CERT_SYSTEM_STORE_CURRENT_USER = 0x00010000 +X509_ASN_ENCODING = 0x00000001 +describe Win32::Certstore, :windows_only do + let(:store_location) { CERT_SYSTEM_STORE_CURRENT_USER } let(:certstore) { Win32::Certstore } - let(:certstore_handler) { Win32::Certstore.new(store_name) } let(:certbase) { Win32::Certstore::StoreBase } + let(:certstore_handler) { Win32::Certstore.new(store_name, store_location: store_location) } describe "#cert_list" do context "When passing empty certificate store name" do let(:store_name) { "" } it "raises ArgumentError" do - expect { certstore.open(store_name) }.to raise_error(ArgumentError, "Empty Certificate Store.") + expect { certstore.open(store_name, store_location: store_location) }.to raise_error(ArgumentError, "Empty Certificate Store.") end end context "When passing invalid certificate store name" do let(:store_name) { "Chef" } it "raises ArgumentError" do - expect { certstore.open(store_name) }.not_to raise_error(ArgumentError, "Empty Certificate Store.") + expect { certstore.open(store_name, store_location: store_location) }.not_to raise_error(ArgumentError, "Empty Certificate Store.") end end context "When passing nil certificate store name" do let(:store_name) { nil } it "raises ArgumentError" do - expect { certstore.open(store_name) }.to raise_error(ArgumentError, "Empty Certificate Store.") + expect { certstore.open(store_name, store_location: store_location) }.to raise_error(ArgumentError, "Empty Certificate Store.") end end @@ -54,7 +63,7 @@ allow_any_instance_of(certbase).to receive(:cert_list).and_return([root_certificate_name]) end it "returns certificate list" do - store = certstore.open(store_name) + store = certstore.open(store_name, store_location: store_location) certificate_list = store.list expect(certificate_list.size).to eql(1) expect(certificate_list.first).to eql root_certificate_name @@ -63,36 +72,34 @@ end describe "#cert_add" do - context "When passing invalid certificate object" do + context "When passing certificate path instead of certificate object to the CurrentUser store" do let(:store_name) { "root" } let(:cert_file_path) { '.\spec\win32\assets\GlobalSignRootCA.pem' } - it "raises ArgumentError - Invalid Certificate object" do - allow(certstore_handler).to receive(:CertAddEncodedCertificateToStore).and_return(false) - allow(certstore).to receive(:open).with(store_name).and_return(certstore_handler) - store = certstore.open(store_name) + it "it raises ArgumentError - Invalid Certificate object" do + store = certstore.open(store_name, store_location: store_location) expect { store.add(cert_file_path) }.to raise_error(ArgumentError) end end - context "When passing certificate path instead of certificate object" do + context "When passing invalid certificate object to the CurrentUser Store" do let(:store_name) { "my" } - let(:cert_file_path) { '.\spec\win32\assets\GlobalSignRootCA.pem' } - - it "raises ArgumentError - Invalid Certificate object" do - allow(certbase).to receive(:CertAddEncodedCertificateToStore).and_return(false) - store = certstore.open(store_name) + let(:cert_file_path) { '.\spec\win32\assets\notes.txt' } + it "it raises ArgumentError - Invalid Certificate object" do + allow(certstore).to receive(:CertAddEncodedCertificateToStore).and_return(false) + store = certstore.open(store_name, store_location: store_location) expect { store.add(cert_file_path) }.to raise_error(ArgumentError) end end - context "When passing valid certificate object" do + context "When passing a valid certificate object to the CurrentUser store" do let(:store_name) { "root" } let(:cert_file_path) { '.\spec\win32\assets\GlobalSignRootCA.pem' } let(:certificate_object) { OpenSSL::X509::Certificate.new(File.read cert_file_path) } + before(:each) do + allow_any_instance_of(certbase).to receive(:cert_add).and_return(true) + end it "returns Certificate added successfully" do - allow(certstore_handler).to receive(:CertAddEncodedCertificateToStore).and_return(true) - allow(certstore).to receive(:open).with(store_name).and_return(certstore_handler) - store = certstore.open(store_name) + store = certstore.open(store_name, store_location: store_location) expect(store.add(certificate_object)).to eql true end end @@ -102,59 +109,56 @@ context "When passing empty certificate store name" do let(:store_name) { "" } it "raises ArgumentError" do - expect { certstore.open(store_name) }.to raise_error(ArgumentError, "Empty Certificate Store.") + expect { certstore.open(store_name, store_location: store_location) }.to raise_error(ArgumentError, "Empty Certificate Store.") end end - context "When passing empty thumbprint" do + context "When passing empty thumbprint to the CurrentUser store" do let(:store_name) { "root" } let(:thumbprint) { " " } - it "raises ArgumentError" do - store = certstore.open(store_name) + it "it raises ArgumentError" do + store = certstore.open(store_name, store_location: store_location) expect { store.get(thumbprint) }.to raise_error(ArgumentError, "Invalid certificate thumbprint.") end end - context "When passing thumbprint is nil" do + context "When passing thumbprint is nil to the CurrentUser store" do let(:store_name) { "root" } let(:thumbprint) { nil } - it "raises ArgumentError" do - store = certstore.open(store_name) + it "it raises ArgumentError" do + store = certstore.open(store_name, store_location: store_location) expect { store.get(thumbprint) }.to raise_error(ArgumentError, "Invalid certificate thumbprint.") end end context "When passing invalid thumbprint" do let(:store_name) { "root" } - let(:thumbprint) { "b1bc968bd4f49d622aa89a81f2150152a41d829c" } - let(:store_location) {} + let(:thumbprint) { "b1bc968bd4f49d622aa89a81f2150152a41d829f" } before(:each) do - allow_any_instance_of(certbase).to receive(:get_cert_pem).and_return("") + allow_any_instance_of(certbase).to receive(:cert_get).and_raise(ArgumentError, "Invalid certificate thumbprint.") end - it "returns empty string" do - store = certstore.open(store_name) - cert_obj = store.get(thumbprint, store_location: CERT_SYSTEM_STORE_CURRENT_USER, store_name: store_name) - expect(cert_obj).to be_empty + it "returns Argument Error" do + store = certstore.open(store_name, store_location: store_location) + # cert_obj = store.get(thumbprint) + expect { store.get(thumbprint) }.to raise_error(ArgumentError, "Invalid certificate thumbprint.") end end - context "When passing valid thumbprint" do - let(:store_name) { "root" } - let(:thumbprint) { "b1bc968bd4f49d622aa89a81f2150152a41d829909c" } - let(:cert_pem) { File.read('.\spec\win32\assets\GlobalSignRootCA.pem') } - before(:each) do - allow_any_instance_of(certbase).to receive(:get_cert_pem).and_return(cert_pem) + context "When passing valid thumbprint to the CurrentUser store" do + let(:store_name) { "MY" } + let(:thumbprint22) { "b1bc968bd4f49d622aa89a81f2150152a41d829c" } + let(:cert_file_path) { '.\spec\win32\assets\GlobalSignRootCA.pem' } + let(:certificate_object) { OpenSSL::X509::Certificate.new(File.read cert_file_path) } + before do + allow_any_instance_of(certbase).to receive(:cert_get).and_return(certificate_object) end - it "returns OpenSSL::X509::Certificate Object" do - store = certstore.open(store_name) - cert_obj = store.get(thumbprint) - expect(cert_obj).to be_an_instance_of(OpenSSL::X509::Certificate) - expect(cert_obj.not_before.to_s).to eql("1998-09-01 12:00:00 UTC") - expect(cert_obj.not_after.to_s).to eql("2028-01-28 12:00:00 UTC") + it "it returns an OpenSSL::X509::Certificate Object from the CurrentUser store" do + store = certstore.open(store_name, store_location: store_location) + expect(store.get(thumbprint22)).to be_an_instance_of(OpenSSL::X509::Certificate) end end - context "When passing valid thumbprint with spaces" do + context "When passing valid thumbprint with spaces to the CurrentUser store" do let(:store_name) { "root" } let(:thumbprint) { "b1 bc 96 8b d4 f4 9d 62 2a a8 9a 81 f2 15 01 52 a4 1d 82 9c" } let(:cert_pem) { File.read('.\spec\win32\assets\GlobalSignRootCA.pem') } @@ -164,13 +168,15 @@ it "returns OpenSSL::X509::Certificate Object" do store = certstore.open(store_name) cert_obj = store.get(thumbprint) + # the cert_obj returned to us ends up being a raw, unformatted cert. Converting the returned object to ensure it IS a correctly formatted cert vs nonsense + cert_obj = OpenSSL::X509::Certificate.new(cert_obj) expect(cert_obj).to be_an_instance_of(OpenSSL::X509::Certificate) expect(cert_obj.not_before.to_s).to eql("1998-09-01 12:00:00 UTC") expect(cert_obj.not_after.to_s).to eql("2028-01-28 12:00:00 UTC") end end - context "When passing valid thumbprint with :" do + context "When passing valid thumbprint with colons to the CurrentUser store" do let(:store_name) { "root" } let(:thumbprint) { "b1:bc:96:8b:d4:f4:9d:62:2a:a8:9a:81:f2:15:01:52:a4:1d:82:9c" } let(:cert_pem) { File.read('.\spec\win32\assets\GlobalSignRootCA.pem') } @@ -180,6 +186,8 @@ it "returns OpenSSL::X509::Certificate Object" do store = certstore.open(store_name) cert_obj = store.get(thumbprint) + # the cert_obj returned to us ends up being a raw, unformatted cert. Converting the returned object to ensure it IS a correctly formatted cert vs nonsense + cert_obj = OpenSSL::X509::Certificate.new(cert_obj) expect(cert_obj).to be_an_instance_of(OpenSSL::X509::Certificate) expect(cert_obj.not_before.to_s).to eql("1998-09-01 12:00:00 UTC") expect(cert_obj.not_after.to_s).to eql("2028-01-28 12:00:00 UTC") @@ -187,161 +195,126 @@ end end - CERT_SYSTEM_STORE_LOCAL_MACHINE = 0x00020000 - describe "#cert_delete" do - context "When passing empty certificate store name" do + context "When passing empty certificate store name to the CurrentUser Store" do let(:store_name) { "" } it "raises ArgumentError" do - expect { certstore.open(store_name) }.to raise_error(ArgumentError, "Empty Certificate Store.") + expect { certstore.open(store_name, store_location: store_location) }.to raise_error(ArgumentError, "Empty Certificate Store.") end end - context "When passing empty thumbprint" do + context "When passing empty thumbprint to the CurrentUser Store" do let(:store_name) { "root" } let(:thumbprint) { " " } it "raises ArgumentError" do - store = certstore.open(store_name) + store = certstore.open(store_name, store_location: store_location) expect { store.delete(thumbprint) }.to raise_error(ArgumentError, "Invalid certificate thumbprint.") end end - context "When passing thumbprint is nil" do + context "When passing thumbprint is nil to the CurrentUser Store" do let(:store_name) { "root" } let(:thumbprint) { nil } it "raises ArgumentError" do - store = certstore.open(store_name) + store = certstore.open(store_name, store_location: store_location) expect { store.delete(thumbprint) }.to raise_error(ArgumentError, "Invalid certificate thumbprint.") end end - context "When passing invalid thumbprint" do + context "When passing invalid thumbprint to the CurrentUser Store" do let(:store_name) { "root" } let(:thumbprint) { "b1bc968bd4f49d622aa89a81f2150152a41d829c" } before(:each) do - allow_any_instance_of(certbase).to receive(:CertFindCertificateInStore).and_return(false) - allow_any_instance_of(certbase).to receive(:lookup_error).and_return(false) + allow_any_instance_of(certstore).to receive(:CertFindCertificateInStore).and_return(false) + allow_any_instance_of(certstore).to receive(:lookup_error).and_return(false) end it "returns false" do - store = certstore.open(store_name) + store = certstore.open(store_name, store_location: store_location) expect(store.delete(thumbprint)).to eql(false) end end - context "When passing valid thumbprint" do + context "When passing valid thumbprint to the CurrentUser Store" do let(:store_name) { "root" } - let(:cert_pem) { File.read('.\spec\win32\assets\GlobalSignRootCA.pem') } before(:each) do - allow_any_instance_of(certbase).to receive(:CertFindCertificateInStore).and_return(FFI::MemoryPointer.new(1)) - allow_any_instance_of(certbase).to receive(:CertDuplicateCertificateContext).and_return(true) - allow_any_instance_of(certbase).to receive(:CertDeleteCertificateFromStore).and_return(true) - allow_any_instance_of(certbase).to receive(:CertFreeCertificateContext).and_return(true) + allow_any_instance_of(certstore).to receive(:CertFindCertificateInStore).and_return(FFI::MemoryPointer.new(1)) + allow_any_instance_of(certstore).to receive(:CertDuplicateCertificateContext).and_return(true) + allow_any_instance_of(certstore).to receive(:CertDeleteCertificateFromStore).and_return(true) + allow_any_instance_of(certstore).to receive(:CertFreeCertificateContext).and_return(true) end it "returns true when thumbprint has no spaces" do - thumbprint = "b1bc968bd4f49d622aa89a81f2150152a41d829909c" - store = certstore.open(store_name) - expect(store.delete(thumbprint)).to eql(true) - end - it "returns true when thumbprint has spaces" do - thumbprint = "b1 bc 96 8b d4 f4 9d 62 2a a8 9a 81 f2 15 01 52 a4 1d 82 9c" - store = certstore.open(store_name) - expect(store.delete(thumbprint)).to eql(true) - end - it "returns true when thumbprint is colon(:) seperated" do - thumbprint = "b1:bc:96:8b:d4:f4:9d:62:2a:a8:9a:81:f2:15:01:52:a4:1d:82:9c" - store = certstore.open(store_name) + thumbprint = "b1bc968bd4f49d622aa89a81f2150152a41d829c" + store = certstore.open(store_name, store_location: store_location) expect(store.delete(thumbprint)).to eql(true) end end - context "When thumbprint not found" do + context "When passing a valid thumbprint with spaces or colons to the CurrentUser store" do let(:store_name) { "root" } - let(:thumbprint) { "invalid_thumbprint" } - before(:each) do - allow_any_instance_of(certbase).to receive(:get_cert_pem).and_return("") + let(:thumbprint) { "b1 bc 96 8b d4 f4 9d 62 2a a8 9a 81 f2 15 01 52 a4 1d 82 9c" } + let(:thumbprint2) { "b1:bc:96:8b:d4:f4:9d:62:2a:a8:9a:81:f2:15:01:52:a4:1d:82:9c" } + before do + allow_any_instance_of(certbase).to receive(:cert_delete).and_return(true) end - it "returns empty string" do - store = certstore.open(store_name) - cert_obj = store.get(thumbprint, store_location: CERT_SYSTEM_STORE_CURRENT_USER, store_name: store_name) - expect(cert_obj).to be_empty + it "returns True when thumbprint has spaces" do + store = certstore.open(store_name, store_location: store_location) + expect(store.delete(thumbprint)).to eql(true) + end + it "returns True when thumbprint has colons" do + store = certstore.open(store_name, store_location: store_location) + expect(store.delete(thumbprint2)).to eql(true) end end end describe "#cert_validate" do - context "When passing empty certificate store name" do + context "When passing empty certificate store name to the CurrentUser store" do let(:store_name) { "" } it "raises ArgumentError" do - expect { certstore.open(store_name) }.to raise_error(ArgumentError, "Empty Certificate Store.") + expect { certstore.open(store_name, store_location: store_location) }.to raise_error(ArgumentError, "Empty Certificate Store.") end end - context "When passing empty thumbprint" do + context "When passing empty thumbprint to the CurrentUser store" do let(:store_name) { "root" } let(:thumbprint) { " " } it "raises ArgumentError" do - store = certstore.open(store_name) + store = certstore.open(store_name, store_location: store_location) expect { store.valid?(thumbprint) }.to raise_error(ArgumentError, "Invalid certificate thumbprint.") end end - context "When passing thumbprint is nil" do + context "When passing thumbprint is nil to the CurrentUser store" do let(:store_name) { "root" } let(:thumbprint) { nil } it "raises ArgumentError" do - store = certstore.open(store_name) + store = certstore.open(store_name, store_location: store_location) expect { store.valid?(thumbprint) }.to raise_error(ArgumentError, "Invalid certificate thumbprint.") end end - context "When passing invalid thumbprint" do + context "When passing valid thumbprint to the CurrentUser store" do let(:store_name) { "root" } let(:thumbprint) { "b1bc968bd4f49d622aa89a81f2150152a41d829c" } before(:each) do - allow_any_instance_of(certbase).to receive(:get_cert_pem).and_return("") - end - it "returns false" do - store = certstore.open(store_name) - expect(store.valid?(thumbprint)).to eql(false) - end - end - - context "When passing valid certificate's thumbprint" do - let(:store_name) { "root" } - let(:thumbprint) { "b1bc968bd4f49d622aa89a81f2150152a41d829909c" } - let(:cert_pem) { File.read('.\spec\win32\assets\GlobalSignRootCA.pem') } - before(:each) do - allow_any_instance_of(certbase).to receive(:get_cert_pem).and_return(cert_pem) - end - it "returns true" do - store = certstore.open(store_name) - expect(store.valid?(thumbprint)).to eql(true) - end - end - - context "When passing valid certificate's thumbprint with spaces" do - let(:store_name) { "root" } - let(:thumbprint) { "b1 bc 96 8b d4 f4 9d 62 2a a8 9a 81 f2 15 01 52 a4 1d 82 9c" } - let(:cert_pem) { File.read('.\spec\win32\assets\GlobalSignRootCA.pem') } - before(:each) do - allow_any_instance_of(certbase).to receive(:get_cert_pem).and_return(cert_pem) + allow_any_instance_of(certbase).to receive(:cert_validate).and_return(true) end - it "returns true" do - store = certstore.open(store_name) + it "returns Certificate found" do + store = certstore.open(store_name, store_location: store_location) expect(store.valid?(thumbprint)).to eql(true) end end - context "When passing valid certificate's thumbprint with :" do + context "When passing a valid certificate thumbprint for a certificate that does not exist to the CurrentUser store" do let(:store_name) { "root" } - let(:thumbprint) { "b1:bc:96:8b:d4:f4:9d:62:2a:a8:9a:81:f2:15:01:52:a4:1d:82:9c" } + let(:thumbprint) { "b1bc968bd4f49d622aa89a81f2150152a41d829f" } let(:cert_pem) { File.read('.\spec\win32\assets\GlobalSignRootCA.pem') } before(:each) do - allow_any_instance_of(certbase).to receive(:get_cert_pem).and_return(cert_pem) + allow_any_instance_of(certbase).to receive(:cert_validate).and_raise(ArgumentError, "Certificate not found") end - it "returns true" do - store = certstore.open(store_name) - expect(store.valid?(thumbprint)).to eql(true) + it "returns Certificate not found" do + store = certstore.open(store_name, store_location: store_location) + expect { store.valid?(thumbprint) }.to raise_error(ArgumentError, "Certificate not found") end end end @@ -351,7 +324,7 @@ let(:store_name) { "root" } let(:token) { " " } it "raises ArgumentError" do - store = certstore.open(store_name) + store = certstore.open(store_name, store_location: store_location) expect { store.search(token) }.to raise_error(ArgumentError, "Invalid search token") end end @@ -359,18 +332,19 @@ let(:store_name) { "root" } let(:token) { nil } it "raises ArgumentError" do - store = certstore.open(store_name) + store = certstore.open(store_name, store_location: store_location) expect { store.search(token) }.to raise_error(ArgumentError, "Invalid search token") end end + context "When passing invalid search token" do let(:store_name) { "root" } let(:token) { "invalid search" } before(:each) do - allow_any_instance_of(certbase).to receive(:get_cert_property).and_return("") + allow_any_instance_of(certstore).to receive(:get_cert_property).and_return("") end it "returns empty Array list" do - store = certstore.open(store_name) + store = certstore.open(store_name, store_location: store_location) cert_list = store.search(token) expect(cert_list).to be_an_instance_of(Array) expect(cert_list).to be_empty @@ -380,10 +354,10 @@ let(:store_name) { "root" } let(:token) { "GlobalSign Root CA" } before(:each) do - allow_any_instance_of(certbase).to receive(:get_cert_property).and_return(["", "", "BE, GlobalSign nv-sa, Root CA, GlobalSign Root CA", "GlobalSign Root CA", "GlobalSign Root CA", "GlobalSign Root CA", "GlobalSign Root CA", "", ""]) + allow_any_instance_of(certstore).to receive(:get_cert_property).and_return(["", "", "BE, GlobalSign nv-sa, Root CA, GlobalSign Root CA", "GlobalSign Root CA", "GlobalSign Root CA", "GlobalSign Root CA", "GlobalSign Root CA", "", ""]) end it "returns valid " do - store = certstore.open(store_name) + store = certstore.open(store_name, store_location: store_location) cert_list = store.search(token) expect(cert_list).to be_an_instance_of(Array) cert_list.flatten! @@ -393,127 +367,100 @@ end end - describe "Perform more than one operations with single certstore object" do - context "Perform add and list with single certstore object" do + describe "#get_thumbprint" do + context "When looking up a certificate by Friendlyname, CN, or Subject Name" do let(:store_name) { "root" } + let(:search_token) { "GlobalSign" } + let(:broken_search_token) { "Foo" } + let(:root_certificate_name) { "Microsoft Root Certificate Authority" } let(:cert_file_path) { '.\spec\win32\assets\GlobalSignRootCA.pem' } let(:certificate_object) { OpenSSL::X509::Certificate.new(File.read cert_file_path) } - let(:root_certificate_name) { "Microsoft Root Certificate Authority" } - before(:each) do - allow_any_instance_of(certbase).to receive(:cert_list).and_return([root_certificate_name]) + let(:thumbprint) { "b1bc968bd4f49d622aa89a81f2150152a41d829c" } + before do + allow_any_instance_of(certbase).to receive(:cert_lookup_by_token).and_return(thumbprint) end - it "returns Certificate added successfully listing certificates for the same" do - allow(certstore_handler).to receive(:CertAddEncodedCertificateToStore).and_return(true) - allow(certstore).to receive(:open).with(store_name).and_return(certstore_handler) - store = certstore.open(store_name) - expect(store.add(certificate_object)).to eql true - certificate_list = store.list - expect(certificate_list.size).to eql(1) - expect(certificate_list.first).to eql root_certificate_name + it "it returns a valid thumbprint if a certificate matches." do + store = certstore.open(store_name, store_location: store_location) + expect(store.get_thumbprint(search_token)).to eql("b1bc968bd4f49d622aa89a81f2150152a41d829c") end end - end - - describe "#Failed with FFI::LastError" do - context "While adding or deleting or retrieving certificate" do + context "When looking up a certificate by Friendlyname, CN, or Subject Name for a non-existent certificate" do let(:store_name) { "root" } + let(:search_token) { "GlobalSign" } + let(:broken_search_token) { "Foo" } + let(:root_certificate_name) { "Microsoft Root Certificate Authority" } let(:cert_file_path) { '.\spec\win32\assets\GlobalSignRootCA.pem' } let(:certificate_object) { OpenSSL::X509::Certificate.new(File.read cert_file_path) } - let(:certificate_name) { "GlobalSign" } let(:thumbprint) { "b1bc968bd4f49d622aa89a81f2150152a41d829c" } - - it "returns 'The operation was canceled by the user'" do - allow(certstore_handler).to receive(:CertAddEncodedCertificateToStore).and_return(false) - allow(FFI::LastError).to receive(:error).and_return(1223) - allow(certstore).to receive(:open).with(store_name).and_return(certstore_handler) - store = certstore.open(store_name) - expect { store.add(certificate_object) }.to raise_error(SystemCallError) + before do + allow_any_instance_of(certbase).to receive(:cert_lookup_by_token).and_raise(ArgumentError, "Certificate not found while looking for certificate : #{search_token} in store : #{store_name} at this location : #{store_location}") end - - it "returns 'Cannot find object or property'" do - allow(certstore_handler).to receive(:CertAddEncodedCertificateToStore).and_return(false) - allow(FFI::LastError).to receive(:error).and_return(-2146885628) - allow(certstore).to receive(:open).with(store_name).and_return(certstore_handler) - store = certstore.open(store_name) - expect { store.add(certificate_object) }.to raise_error(SystemCallError) + it "Throws an Argument error when the matching certificate does not exist" do + store = certstore.open(store_name, store_location: store_location) + expect { store.get_thumbprint(broken_search_token) }.to raise_error(ArgumentError, "Certificate not found while looking for certificate : #{search_token} in store : #{store_name} at this location : #{store_location}") end + end + end - it "returns 'An error occurred while reading or writing to a file'" do - allow(certstore_handler).to receive(:CertAddEncodedCertificateToStore).and_return(false) - allow(FFI::LastError).to receive(:error).and_return(-2146885629) - allow(certstore).to receive(:open).with(store_name).and_return(certstore_handler) - store = certstore.open(store_name) - expect { store.add(certificate_object) }.to raise_error(SystemCallError) - end + private - it "returns 'ASN1 bad tag value met. -- Is the certificate in DER format?'" do - allow(certstore_handler).to receive(:CertAddEncodedCertificateToStore).and_return(false) - allow(FFI::LastError).to receive(:error).and_return(-2146881269) - allow(certstore).to receive(:open).with(store_name).and_return(certstore_handler) - store = certstore.open(store_name) - expect { store.add(certificate_object) }.to raise_error(SystemCallError) - end + def open_cert_store(store, store_location) + @store = Win32::Certstore.open(store, store_location: store_location) + end - it "returns 'ASN1 unexpected end of data'" do - allow(certstore_handler).to receive(:CertAddEncodedCertificateToStore).and_return(false) - allow(certstore).to receive(:open).with(store_name).and_return(certstore_handler) - allow(FFI::LastError).to receive(:error).and_return(-2146881278) - store = certstore.open(store_name) - expect { store.add(certificate_object) }.to raise_error(SystemCallError) - end + def add_cert + raw = File.read ".\\spec\\win32\\assets\\GlobalSignRootCA.pem" + certificate_object = OpenSSL::X509::Certificate.new raw + @store.add(certificate_object) + end - it "return 'System.UnauthorizedAccessException, Access denied..'" do - allow(certbase).to receive(:CertFindCertificateInStore).and_return(true) - allow(certbase).to receive(:CertDeleteCertificateFromStore).and_return(false) - allow(FFI::LastError).to receive(:error).and_return(-2147024891) - store = certstore.open(store_name) - expect { store.delete(thumbprint) }.not_to raise_error(SystemCallError) - end - end + def add_pfx + raw = ".\\spec\\win32\\assets\\steveb.pfx" + @store.add_pfx(raw, "1234") end -end -# Begin Testing for store_location + def close_store + @store.close + end -CERT_SYSTEM_STORE_CURRENT_USER = 0x00010000 + def delete_cert + @store.delete("b1bc968bd4f49d622aa89a81f2150152a41d829c") + end +end describe Win32::Certstore, :windows_only do - + let(:store_location) { CERT_SYSTEM_STORE_LOCAL_MACHINE } let(:certstore) { Win32::Certstore } - let(:certstore_handler) { Win32::Certstore.new(store_name, store_location: CERT_SYSTEM_STORE_CURRENT_USER) } let(:certbase) { Win32::Certstore::StoreBase } + let(:certstore_handler) { Win32::Certstore.new(store_name, store_location: store_location) } describe "#cert_list" do - context "When passing empty certificate store name" do + context "When passing empty certificate store name to the LocalMachine store" do let(:store_name) { "" } - let(:store_location) { CERT_SYSTEM_STORE_CURRENT_USER } it "raises ArgumentError" do expect { certstore.open(store_name, store_location: store_location) }.to raise_error(ArgumentError, "Empty Certificate Store.") end end - context "When passing invalid certificate store name" do + context "When passing invalid certificate store name to the LocalMachine store" do let(:store_name) { "Chef" } - let(:store_location) { CERT_SYSTEM_STORE_CURRENT_USER } it "raises ArgumentError" do expect { certstore.open(store_name, store_location: store_location) }.not_to raise_error(ArgumentError, "Empty Certificate Store.") end end - context "When passing nil certificate store name" do + context "When passing nil certificate store name to the LocalMachine store" do let(:store_name) { nil } - let(:store_location) { CERT_SYSTEM_STORE_CURRENT_USER } it "raises ArgumentError" do expect { certstore.open(store_name, store_location: store_location) }.to raise_error(ArgumentError, "Empty Certificate Store.") end end - context "When passing valid certificate" do + context "When passing valid certificate to the LocalMachine store" do let(:store_name) { "root" } - let(:store_location) { CERT_SYSTEM_STORE_CURRENT_USER } let(:root_certificate_name) { "Microsoft Root Certificate Authority" } before(:each) do - allow_any_instance_of(certbase).to receive(:cert_list).and_return([root_certificate_name]) + allow_any_instance_of(certstore).to receive(:cert_list).and_return([root_certificate_name]) end it "returns certificate list" do store = certstore.open(store_name, store_location: store_location) @@ -525,38 +472,33 @@ end describe "#cert_add" do - context "When passing invalid certificate object" do + context "When passing certificate path instead of certificate object to the LocalMachine store" do let(:store_name) { "root" } - let(:store_location) { CERT_SYSTEM_STORE_CURRENT_USER } let(:cert_file_path) { '.\spec\win32\assets\GlobalSignRootCA.pem' } - it "raises ArgumentError - Invalid Certificate object" do - allow(certstore_handler).to receive(:CertAddEncodedCertificateToStore).and_return(false) - allow(certstore).to receive(:open).with(store_name, store_location: store_location).and_return(certstore_handler) + it "it raises ArgumentError - Invalid Certificate object" do store = certstore.open(store_name, store_location: store_location) expect { store.add(cert_file_path) }.to raise_error(ArgumentError) end end - context "When passing certificate path instead of certificate object" do + context "When passing invalid certificate object to the LocalMachine Store" do let(:store_name) { "my" } - let(:store_location) { CERT_SYSTEM_STORE_CURRENT_USER } - let(:cert_file_path) { '.\spec\win32\assets\GlobalSignRootCA.pem' } - - it "raises ArgumentError - Invalid Certificate object" do - allow(certbase).to receive(:CertAddEncodedCertificateToStore).and_return(false) + let(:cert_file_path) { '.\spec\win32\assets\notes.txt' } + it "it raises ArgumentError - Invalid Certificate object" do + allow(certstore).to receive(:CertAddEncodedCertificateToStore).and_return(false) store = certstore.open(store_name, store_location: store_location) expect { store.add(cert_file_path) }.to raise_error(ArgumentError) end end - context "When passing valid certificate object" do + context "When passing a valid certificate object to the LocalMachine store" do let(:store_name) { "root" } - let(:store_location) { CERT_SYSTEM_STORE_CURRENT_USER } let(:cert_file_path) { '.\spec\win32\assets\GlobalSignRootCA.pem' } let(:certificate_object) { OpenSSL::X509::Certificate.new(File.read cert_file_path) } + before(:each) do + allow_any_instance_of(certbase).to receive(:cert_add).and_return(true) + end it "returns Certificate added successfully" do - allow(certstore_handler).to receive(:CertAddEncodedCertificateToStore).and_return(true) - allow(certstore).to receive(:open).with(store_name, store_location: store_location).and_return(certstore_handler) store = certstore.open(store_name, store_location: store_location) expect(store.add(certificate_object)).to eql true end @@ -564,112 +506,97 @@ end describe "#cert_get" do - context "When passing empty certificate store name" do + context "When passing empty certificate store name to the LocalMachine store" do let(:store_name) { "" } - let(:store_location) { CERT_SYSTEM_STORE_CURRENT_USER } it "raises ArgumentError" do expect { certstore.open(store_name, store_location: store_location) }.to raise_error(ArgumentError, "Empty Certificate Store.") end end - context "When passing empty thumbprint" do + context "When passing empty thumbprint to the LocalMachine store" do let(:store_name) { "root" } - let(:store_location) { CERT_SYSTEM_STORE_CURRENT_USER } let(:thumbprint) { " " } - it "raises ArgumentError" do + it "it raises ArgumentError" do store = certstore.open(store_name, store_location: store_location) expect { store.get(thumbprint) }.to raise_error(ArgumentError, "Invalid certificate thumbprint.") end end - context "When passing thumbprint is nil" do + context "When passing thumbprint is nil to the LocalMachine store" do let(:store_name) { "root" } - let(:store_location) { CERT_SYSTEM_STORE_CURRENT_USER } let(:thumbprint) { nil } - it "raises ArgumentError" do + it "it raises ArgumentError" do store = certstore.open(store_name, store_location: store_location) expect { store.get(thumbprint) }.to raise_error(ArgumentError, "Invalid certificate thumbprint.") end end - context "When passing invalid thumbprint" do - let(:store_name) { "root" } - let(:store_location) { CERT_SYSTEM_STORE_CURRENT_USER } - let(:thumbprint) { "b1bc968bd4f49d622aa89a81f2150152a41d829c" } + context "When passing invalid thumbprint to the LocalMachine store" do + let(:store_name) { "MY" } + let(:thumbprint) { "Foo" } before(:each) do - allow_any_instance_of(certbase).to receive(:get_cert_pem).and_return("") + allow_any_instance_of(certbase).to receive(:cert_get).and_raise(ArgumentError, "Invalid certificate thumbprint.") end - it "returns empty string" do + it "it raises ArgumentError" do store = certstore.open(store_name, store_location: store_location) - cert_obj = store.get(thumbprint, store_location: CERT_SYSTEM_STORE_CURRENT_USER, store_name: store_name) - expect(cert_obj).to be_empty + expect { store.get(thumbprint) }.to raise_error(ArgumentError, "Invalid certificate thumbprint.") end end - context "When passing valid thumbprint" do - let(:store_name) { "root" } - let(:store_location) { CERT_SYSTEM_STORE_CURRENT_USER } - let(:thumbprint) { "b1bc968bd4f49d622aa89a81f2150152a41d829909c" } - let(:cert_pem) { File.read('.\spec\win32\assets\GlobalSignRootCA.pem') } - before(:each) do - allow_any_instance_of(certbase).to receive(:get_cert_pem).and_return(cert_pem) + context "When passing valid thumbprint top the LocalMachine store" do + let(:store_name) { "MY" } + let(:thumbprint22) { "b1bc968bd4f49d622aa89a81f2150152a41d829c" } + let(:cert_file_path) { '.\spec\win32\assets\GlobalSignRootCA.pem' } + let(:certificate_object) { OpenSSL::X509::Certificate.new(File.read cert_file_path) } + before do + allow_any_instance_of(certbase).to receive(:cert_get).and_return(certificate_object) end - it "returns OpenSSL::X509::Certificate Object" do + it "it returns an OpenSSL::X509::Certificate Object from the CurrentUser store" do store = certstore.open(store_name, store_location: store_location) - cert_obj = store.get(thumbprint) - expect(cert_obj).to be_an_instance_of(OpenSSL::X509::Certificate) - expect(cert_obj.not_before.to_s).to eql("1998-09-01 12:00:00 UTC") - expect(cert_obj.not_after.to_s).to eql("2028-01-28 12:00:00 UTC") + expect(store.get(thumbprint22)).to be_an_instance_of(OpenSSL::X509::Certificate) end end - context "When passing valid thumbprint with spaces" do + # here + context "When passing an invalid thumbprint with spaces to the LocalMachine store" do let(:store_name) { "root" } - let(:store_location) { CERT_SYSTEM_STORE_CURRENT_USER } let(:thumbprint) { "b1 bc 96 8b d4 f4 9d 62 2a a8 9a 81 f2 15 01 52 a4 1d 82 9c" } - let(:cert_pem) { File.read('.\spec\win32\assets\GlobalSignRootCA.pem') } + let(:cert_file_path) { '.\spec\win32\assets\GlobalSignRootCA.pem' } + let(:certificate_object) { OpenSSL::X509::Certificate.new(File.read cert_file_path) } before(:each) do - allow_any_instance_of(certbase).to receive(:get_cert_pem).and_return(cert_pem) + allow_any_instance_of(certbase).to receive(:cert_get).and_return(certificate_object) end - it "returns OpenSSL::X509::Certificate Object" do + it "it raises ArgumentErrore" do store = certstore.open(store_name, store_location: store_location) - cert_obj = store.get(thumbprint) - expect(cert_obj).to be_an_instance_of(OpenSSL::X509::Certificate) - expect(cert_obj.not_before.to_s).to eql("1998-09-01 12:00:00 UTC") - expect(cert_obj.not_after.to_s).to eql("2028-01-28 12:00:00 UTC") + expect(store.get(thumbprint)).to be_an_instance_of(OpenSSL::X509::Certificate) end end - context "When passing valid thumbprint with :" do + context "When passing valid thumbprint with : to the LocalMachine store" do let(:store_name) { "root" } - let(:store_location) { CERT_SYSTEM_STORE_CURRENT_USER } let(:thumbprint) { "b1:bc:96:8b:d4:f4:9d:62:2a:a8:9a:81:f2:15:01:52:a4:1d:82:9c" } - let(:cert_pem) { File.read('.\spec\win32\assets\GlobalSignRootCA.pem') } + let(:cert_file_path) { '.\spec\win32\assets\GlobalSignRootCA.pem' } + let(:certificate_object) { OpenSSL::X509::Certificate.new(File.read cert_file_path) } before(:each) do - allow_any_instance_of(certbase).to receive(:get_cert_pem).and_return(cert_pem) + allow_any_instance_of(certbase).to receive(:cert_get).and_return(certificate_object) end - it "returns OpenSSL::X509::Certificate Object" do + it "it raises ArgumentError" do store = certstore.open(store_name, store_location: store_location) - cert_obj = store.get(thumbprint) - expect(cert_obj).to be_an_instance_of(OpenSSL::X509::Certificate) - expect(cert_obj.not_before.to_s).to eql("1998-09-01 12:00:00 UTC") - expect(cert_obj.not_after.to_s).to eql("2028-01-28 12:00:00 UTC") + expect(store.get(thumbprint)).to be_an_instance_of(OpenSSL::X509::Certificate) end end end describe "#cert_delete" do - context "When passing empty certificate store name" do + context "When passing empty certificate store name to the LocalMachine Store" do let(:store_name) { "" } - let(:store_location) { CERT_SYSTEM_STORE_CURRENT_USER } it "raises ArgumentError" do expect { certstore.open(store_name, store_location: store_location) }.to raise_error(ArgumentError, "Empty Certificate Store.") end end - context "When passing empty thumbprint" do + context "When passing empty thumbprint to the LocalMachine Store" do let(:store_name) { "root" } - let(:store_location) { CERT_SYSTEM_STORE_CURRENT_USER } let(:thumbprint) { " " } it "raises ArgumentError" do store = certstore.open(store_name, store_location: store_location) @@ -677,9 +604,8 @@ end end - context "When passing thumbprint is nil" do + context "When passing thumbprint is nil to the LocalMachine Store" do let(:store_name) { "root" } - let(:store_location) { CERT_SYSTEM_STORE_CURRENT_USER } let(:thumbprint) { nil } it "raises ArgumentError" do store = certstore.open(store_name, store_location: store_location) @@ -687,13 +613,12 @@ end end - context "When passing invalid thumbprint" do + context "When passing invalid thumbprint to the LocalMachine Store" do let(:store_name) { "root" } - let(:store_location) { CERT_SYSTEM_STORE_CURRENT_USER } let(:thumbprint) { "b1bc968bd4f49d622aa89a81f2150152a41d829c" } before(:each) do - allow_any_instance_of(certbase).to receive(:CertFindCertificateInStore).and_return(false) - allow_any_instance_of(certbase).to receive(:lookup_error).and_return(false) + allow_any_instance_of(certstore).to receive(:CertFindCertificateInStore).and_return(false) + allow_any_instance_of(certstore).to receive(:lookup_error).and_return(false) end it "returns false" do store = certstore.open(store_name, store_location: store_location) @@ -701,129 +626,46 @@ end end - context "When passing valid thumbprint" do + context "When passing valid thumbprint to the LocalMachine Store" do let(:store_name) { "root" } - let(:store_location) { CERT_SYSTEM_STORE_CURRENT_USER } - let(:cert_pem) { File.read('.\spec\win32\assets\GlobalSignRootCA.pem') } + # let(:cert_pem) { File.read('.\spec\win32\assets\GlobalSignRootCA.pem') } before(:each) do - allow_any_instance_of(certbase).to receive(:CertFindCertificateInStore).and_return(FFI::MemoryPointer.new(1)) - allow_any_instance_of(certbase).to receive(:CertDuplicateCertificateContext).and_return(true) - allow_any_instance_of(certbase).to receive(:CertDeleteCertificateFromStore).and_return(true) - allow_any_instance_of(certbase).to receive(:CertFreeCertificateContext).and_return(true) + allow_any_instance_of(certstore).to receive(:CertFindCertificateInStore).and_return(FFI::MemoryPointer.new(1)) + allow_any_instance_of(certstore).to receive(:CertDuplicateCertificateContext).and_return(true) + allow_any_instance_of(certstore).to receive(:CertDeleteCertificateFromStore).and_return(true) + allow_any_instance_of(certstore).to receive(:CertFreeCertificateContext).and_return(true) end it "returns true when thumbprint has no spaces" do - thumbprint = "b1bc968bd4f49d622aa89a81f2150152a41d829909c" - store = certstore.open(store_name, store_location: store_location) - expect(store.delete(thumbprint)).to eql(true) - end - it "returns true when thumbprint has spaces" do - thumbprint = "b1 bc 96 8b d4 f4 9d 62 2a a8 9a 81 f2 15 01 52 a4 1d 82 9c" - store = certstore.open(store_name, store_location: store_location) - expect(store.delete(thumbprint)).to eql(true) - end - it "returns true when thumbprint is colon(:) seperated" do - thumbprint = "b1:bc:96:8b:d4:f4:9d:62:2a:a8:9a:81:f2:15:01:52:a4:1d:82:9c" + thumbprint = "b1bc968bd4f49d622aa89a81f2150152a41d829c" store = certstore.open(store_name, store_location: store_location) expect(store.delete(thumbprint)).to eql(true) end end - context "When thumbprint not found" do + context "When passing a valid thumbprint with spaces or colons to the LocalMachine store" do let(:store_name) { "root" } - let(:store_location) { CERT_SYSTEM_STORE_CURRENT_USER } - let(:thumbprint) { "invalid_thumbprint" } - before(:each) do - allow_any_instance_of(certbase).to receive(:get_cert_pem).and_return("") - end - it "returns empty string" do - store = certstore.open(store_name, store_location: store_location) - cert_obj = store.get(thumbprint, store_location: CERT_SYSTEM_STORE_CURRENT_USER, store_name: store_name) - expect(cert_obj).to be_empty - end - end - end - - describe "#cert_validate" do - context "When passing empty certificate store name" do - let(:store_name) { "" } - let(:store_location) { CERT_SYSTEM_STORE_CURRENT_USER } - it "raises ArgumentError" do - expect { certstore.open(store_name, store_location: store_location) }.to raise_error(ArgumentError, "Empty Certificate Store.") - end - end - - context "When passing empty thumbprint" do - let(:store_name) { "root" } - let(:store_location) { CERT_SYSTEM_STORE_CURRENT_USER } - let(:thumbprint) { " " } - it "raises ArgumentError" do - store = certstore.open(store_name, store_location: store_location) - expect { store.valid?(thumbprint) }.to raise_error(ArgumentError, "Invalid certificate thumbprint.") - end - end - - context "When passing thumbprint is nil" do - let(:store_name) { "root" } - let(:store_location) { CERT_SYSTEM_STORE_CURRENT_USER } - let(:thumbprint) { nil } - it "raises ArgumentError" do - store = certstore.open(store_name, store_location: store_location) - expect { store.valid?(thumbprint) }.to raise_error(ArgumentError, "Invalid certificate thumbprint.") - end - end - - context "When passing invalid thumbprint" do - let(:store_name) { "root" } - let(:store_location) { CERT_SYSTEM_STORE_CURRENT_USER } - let(:thumbprint) { "b1bc968bd4f49d622aa89a81f2150152a41d829c" } - before(:each) do - allow_any_instance_of(certbase).to receive(:get_cert_pem).and_return("") - end - it "returns false" do - store = certstore.open(store_name, store_location: store_location) - expect(store.valid?(thumbprint)).to eql(false) - end - end - - context "When passing valid certificate's thumbprint" do - let(:store_name) { "root" } - let(:store_location) { CERT_SYSTEM_STORE_CURRENT_USER } - let(:thumbprint) { "b1bc968bd4f49d622aa89a81f2150152a41d829909c" } - let(:cert_pem) { File.read('.\spec\win32\assets\GlobalSignRootCA.pem') } + let(:thumbprint) { "b1 bc 96 8b d4 f4 9d 62 2a a8 9a 81 f2 15 01 52 a4 1d 82 9c" } + let(:thumbprint2) { "b1:bc:96:8b:d4:f4:9d:62:2a:a8:9a:81:f2:15:01:52:a4:1d:82:9c" } before(:each) do - allow_any_instance_of(certbase).to receive(:get_cert_pem).and_return(cert_pem) + allow_any_instance_of(certbase).to receive(:cert_delete).and_return(true) end - it "returns true" do + it "returns True when thumbprint has spaces" do store = certstore.open(store_name, store_location: store_location) - expect(store.valid?(thumbprint)).to eql(true) - end - end - - context "When passing valid certificate's thumbprint with spaces" do - let(:store_name) { "root" } - let(:store_location) { CERT_SYSTEM_STORE_CURRENT_USER } - let(:thumbprint) { "b1 bc 96 8b d4 f4 9d 62 2a a8 9a 81 f2 15 01 52 a4 1d 82 9c" } - let(:cert_pem) { File.read('.\spec\win32\assets\GlobalSignRootCA.pem') } - before(:each) do - allow_any_instance_of(certbase).to receive(:get_cert_pem).and_return(cert_pem) + expect(store.delete(thumbprint)).to eql(true) end - it "returns true" do + it "returns True when thumbprint has colons" do store = certstore.open(store_name, store_location: store_location) - expect(store.valid?(thumbprint)).to eql(true) + expect(store.delete(thumbprint2)).to eql(true) end end - context "When passing valid certificate's thumbprint with :" do + context "When passing a valid certificate thumbprint for a certificate that does not exist to the LocalMachine store" do let(:store_name) { "root" } - let(:store_location) { CERT_SYSTEM_STORE_CURRENT_USER } - let(:thumbprint) { "b1:bc:96:8b:d4:f4:9d:62:2a:a8:9a:81:f2:15:01:52:a4:1d:82:9c" } + let(:thumbprint) { "b1bc968bd4f49d622aa89a81f2150152a41d829f" } let(:cert_pem) { File.read('.\spec\win32\assets\GlobalSignRootCA.pem') } - before(:each) do - allow_any_instance_of(certbase).to receive(:get_cert_pem).and_return(cert_pem) - end - it "returns true" do + it "returns Certificate not found" do store = certstore.open(store_name, store_location: store_location) - expect(store.valid?(thumbprint)).to eql(true) + expect(store.delete(thumbprint)).to eql(false) end end end @@ -831,7 +673,6 @@ describe "#cert_search" do context "When passing empty token" do let(:store_name) { "root" } - let(:store_location) { CERT_SYSTEM_STORE_CURRENT_USER } let(:token) { " " } it "raises ArgumentError" do store = certstore.open(store_name, store_location: store_location) @@ -840,7 +681,6 @@ end context "When search token is nil" do let(:store_name) { "root" } - let(:store_location) { CERT_SYSTEM_STORE_CURRENT_USER } let(:token) { nil } it "raises ArgumentError" do store = certstore.open(store_name, store_location: store_location) @@ -849,10 +689,9 @@ end context "When passing invalid search token" do let(:store_name) { "root" } - let(:store_location) { CERT_SYSTEM_STORE_CURRENT_USER } let(:token) { "invalid search" } before(:each) do - allow_any_instance_of(certbase).to receive(:get_cert_property).and_return("") + allow_any_instance_of(certstore).to receive(:get_cert_property).and_return("") end it "returns empty Array list" do store = certstore.open(store_name, store_location: store_location) @@ -863,10 +702,9 @@ end context "When passing valid search token CN" do let(:store_name) { "root" } - let(:store_location) { CERT_SYSTEM_STORE_CURRENT_USER } let(:token) { "GlobalSign Root CA" } before(:each) do - allow_any_instance_of(certbase).to receive(:get_cert_property).and_return(["", "", "BE, GlobalSign nv-sa, Root CA, GlobalSign Root CA", "GlobalSign Root CA", "GlobalSign Root CA", "GlobalSign Root CA", "GlobalSign Root CA", "", ""]) + allow_any_instance_of(certstore).to receive(:get_cert_property).and_return(["", "", "BE, GlobalSign nv-sa, Root CA, GlobalSign Root CA", "GlobalSign Root CA", "GlobalSign Root CA", "GlobalSign Root CA", "GlobalSign Root CA", "", ""]) end it "returns valid " do store = certstore.open(store_name, store_location: store_location) @@ -879,84 +717,63 @@ end end - describe "Perform more than one operations with single certstore object" do - context "Perform add and list with single certstore object" do + describe "#get_thumbprint" do + context "When looking up a certificate by Friendlyname, CN, or Subject Name" do let(:store_name) { "root" } - let(:store_location) { CERT_SYSTEM_STORE_CURRENT_USER } + let(:search_token) { "GlobalSign" } + let(:broken_search_token) { "Foo" } + let(:root_certificate_name) { "Microsoft Root Certificate Authority" } let(:cert_file_path) { '.\spec\win32\assets\GlobalSignRootCA.pem' } let(:certificate_object) { OpenSSL::X509::Certificate.new(File.read cert_file_path) } - let(:root_certificate_name) { "Microsoft Root Certificate Authority" } - before(:each) do - allow_any_instance_of(certbase).to receive(:cert_list).and_return([root_certificate_name]) + let(:thumbprint) { "b1bc968bd4f49d622aa89a81f2150152a41d829c" } + before do + allow_any_instance_of(certbase).to receive(:cert_lookup_by_token).and_return(thumbprint) end - it "returns Certificate added successfully listing certificates for the same" do - allow(certstore_handler).to receive(:CertAddEncodedCertificateToStore).and_return(true) - allow(certstore).to receive(:open).with(store_name, store_location: store_location).and_return(certstore_handler) + it "it returns a valid thumbprint if a certificate matches." do store = certstore.open(store_name, store_location: store_location) - expect(store.add(certificate_object)).to eql true - certificate_list = store.list - expect(certificate_list.size).to eql(1) - expect(certificate_list.first).to eql root_certificate_name + expect(store.get_thumbprint(search_token)).to eql("b1bc968bd4f49d622aa89a81f2150152a41d829c") end end - end - - describe "#Failed with FFI::LastError" do - context "While adding or deleting or retrieving certificate" do + context "When looking up a certificate by Friendlyname, CN, or Subject Name for a non-existent certificate" do let(:store_name) { "root" } - let(:store_location) { CERT_SYSTEM_STORE_CURRENT_USER } + let(:search_token) { "GlobalSign" } + let(:broken_search_token) { "Foo" } + let(:root_certificate_name) { "Microsoft Root Certificate Authority" } let(:cert_file_path) { '.\spec\win32\assets\GlobalSignRootCA.pem' } let(:certificate_object) { OpenSSL::X509::Certificate.new(File.read cert_file_path) } - let(:certificate_name) { "GlobalSign" } let(:thumbprint) { "b1bc968bd4f49d622aa89a81f2150152a41d829c" } - - it "returns 'The operation was canceled by the user'" do - allow(certstore_handler).to receive(:CertAddEncodedCertificateToStore).and_return(false) - allow(FFI::LastError).to receive(:error).and_return(1223) - allow(certstore).to receive(:open).with(store_name, store_location: store_location).and_return(certstore_handler) - store = certstore.open(store_name, store_location: store_location) - expect { store.add(certificate_object) }.to raise_error(SystemCallError) + before do + allow_any_instance_of(certbase).to receive(:cert_lookup_by_token).and_raise(ArgumentError, "Certificate not found while looking for certificate : #{search_token} in store : #{store_name} at this location : #{store_location}") end - - it "returns 'Cannot find object or property'" do - allow(certstore_handler).to receive(:CertAddEncodedCertificateToStore).and_return(false) - allow(FFI::LastError).to receive(:error).and_return(-2146885628) - allow(certstore).to receive(:open).with(store_name, store_location: store_location).and_return(certstore_handler) + it "Throws an Argument error when the matching certificate does not exist" do store = certstore.open(store_name, store_location: store_location) - expect { store.add(certificate_object) }.to raise_error(SystemCallError) + expect { store.get_thumbprint(broken_search_token) }.to raise_error(ArgumentError, "Certificate not found while looking for certificate : #{search_token} in store : #{store_name} at this location : #{store_location}") end + end + end - it "returns 'An error occurred while reading or writing to a file'" do - allow(certstore_handler).to receive(:CertAddEncodedCertificateToStore).and_return(false) - allow(FFI::LastError).to receive(:error).and_return(-2146885629) - allow(certstore).to receive(:open).with(store_name, store_location: store_location).and_return(certstore_handler) - store = certstore.open(store_name, store_location: store_location) - expect { store.add(certificate_object) }.to raise_error(SystemCallError) - end + private - it "returns 'ASN1 bad tag value met. -- Is the certificate in DER format?'" do - allow(certstore_handler).to receive(:CertAddEncodedCertificateToStore).and_return(false) - allow(FFI::LastError).to receive(:error).and_return(-2146881269) - allow(certstore).to receive(:open).with(store_name, store_location: store_location).and_return(certstore_handler) - store = certstore.open(store_name, store_location: store_location) - expect { store.add(certificate_object) }.to raise_error(SystemCallError) - end + def open_cert_store(store, store_location) + @store = Win32::Certstore.open(store, store_location: store_location) + end - it "returns 'ASN1 unexpected end of data'" do - allow(certstore_handler).to receive(:CertAddEncodedCertificateToStore).and_return(false) - allow(certstore).to receive(:open).with(store_name, store_location: store_location).and_return(certstore_handler) - allow(FFI::LastError).to receive(:error).and_return(-2146881278) - store = certstore.open(store_name, store_location: store_location) - expect { store.add(certificate_object) }.to raise_error(SystemCallError) - end + def add_cert + raw = File.read ".\\spec\\win32\\assets\\GlobalSignRootCA.pem" + certificate_object = OpenSSL::X509::Certificate.new raw + @store.add(certificate_object) + end - it "return 'System.UnauthorizedAccessException, Access denied..'" do - allow(certbase).to receive(:CertFindCertificateInStore).and_return(true) - allow(certbase).to receive(:CertDeleteCertificateFromStore).and_return(false) - allow(FFI::LastError).to receive(:error).and_return(-2147024891) - store = certstore.open(store_name, store_location: store_location) - expect { store.delete(thumbprint) }.not_to raise_error(SystemCallError) - end - end + def add_pfx + raw = ".\\spec\\win32\\assets\\steveb.pfx" + @store.add_pfx(raw, "1234") + end + + def close_store + @store.close + end + + def delete_cert + @store.delete("b1bc968bd4f49d622aa89a81f2150152a41d829c") end end diff --git a/spec/win32/unit/store/shell_exec_spec.rb b/spec/win32/unit/store/shell_exec_spec.rb deleted file mode 100644 index 28e3289..0000000 --- a/spec/win32/unit/store/shell_exec_spec.rb +++ /dev/null @@ -1,22 +0,0 @@ -require "spec_helper" -require "win32/certstore/mixin/shell_exec" - -describe Win32::Certstore::Mixin::ShellExec do - let(:string_class) { Class.new { include Win32::Certstore::Mixin::ShellExec } } - subject(:string_obj) { string_class.new } - - context "when testing individual methods" do - describe "#shell_out_Command" do - it "executes shellout command" do - cmd = "echo '#{rand(1000)}'" - expect(string_obj).to receive(:shell_out_Command).with(cmd).and_return(true) - string_obj.shell_out_Command(cmd) - end - - it "raises Mixlib::ShellOut::ShellCommandFailed error if invalid command is passed" do - cmd = "powershell.exe -Command -in 'abc'" - expect { string_obj.shell_out_command(cmd) }.to raise_error(Mixlib::ShellOut::ShellCommandFailed) - end - end - end -end diff --git a/spec/win32/unit/store_base_spec.rb b/spec/win32/unit/store_base_spec.rb index 0d0a209..a92de62 100644 --- a/spec/win32/unit/store_base_spec.rb +++ b/spec/win32/unit/store_base_spec.rb @@ -53,7 +53,7 @@ context "other than PFX certificates" do it "raises an error" do expect { subject.cert_add_pfx(certstore_handler, pem_path, password) } - .to raise_error(SystemCallError, "An error occurred during encode or decode operation. - Unable to Add a PFX certificate.") + .to raise_error(SystemCallError) end end context "valid arguments" do diff --git a/win32-certstore.gemspec b/win32-certstore.gemspec index d71fe37..2527b1b 100644 --- a/win32-certstore.gemspec +++ b/win32-certstore.gemspec @@ -19,7 +19,7 @@ Gem::Specification.new do |spec| spec.add_development_dependency "bundler" spec.add_development_dependency "rspec", "~> 3.0" - spec.add_dependency "mixlib-shellout" spec.add_dependency "ffi" + spec.add_runtime_dependency "chef-powershell" spec.metadata["yard.run"] = "yri" end