diff --git a/Gemfile b/Gemfile index 9337c4c..dca768b 100644 --- a/Gemfile +++ b/Gemfile @@ -3,5 +3,4 @@ source 'https://rubygems.org' # Specify your gem's dependencies in win32-certstore.gemspec gemspec -gem 'rb-readline', '~> 0.5.3' -gem 'pry' +gem 'rb-readline' diff --git a/lib/win32-certstore.rb b/lib/win32-certstore.rb index 67e109a..c65bab7 100644 --- a/lib/win32-certstore.rb +++ b/lib/win32-certstore.rb @@ -1,5 +1,21 @@ $LOAD_PATH.unshift File.expand_path('../../lib/win32', __FILE__) +# +# Author:: Nimisha Sharad () +# 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 "store/crypto" +require "mixin/crypto" require "certstore/certstore" require "win32-certstore/version" diff --git a/lib/win32/certstore/certstore.rb b/lib/win32/certstore/certstore.rb index d77d35a..ed84c3f 100644 --- a/lib/win32/certstore/certstore.rb +++ b/lib/win32/certstore/certstore.rb @@ -15,48 +15,59 @@ # See the License for the specific language governing permissions and # limitations under the License. -require 'store/crypto' -require 'store/assertions' +require 'mixin/crypto' +require 'mixin/assertions' require_relative 'store_base' module Win32 class Certstore - include Win32::Store::Crypto - extend Win32::Store::Assertions + include Win32::Mixin::Crypto + extend Win32::Mixin::Assertions include Chef::Mixin::WideString - extend Win32::Certstore::StoreBase + include Win32::Certstore::StoreBase - attr_accessor :store_name + attr_reader :store_name + + def initialize(store_name) + @certstore_handler = open(store_name) + end def self.open(store_name) validate_store(store_name) - @certstore_handle = self.new.send(:open, store_name) if block_given? - yield self + yield self.new(store_name) else - self + self.new(store_name) end end - def self.list - list = cert_list(@certstore_handle) - self.new.send(:close) + def list + list = cert_list(@certstore_handler) + close return list end + def add(cert_file_path) + add = cert_add(@certstore_handler, cert_file_path) + close + return add + end + private + + attr_reader :certstore_handler def open(store_name) - certstore_handle = CertOpenSystemStoreW(nil, wstring(store_name)) - unless certstore_handle + certstore_handler = CertOpenSystemStoreW(nil, wstring(store_name)) + unless certstore_handler last_error = FFI::LastError.error raise Chef::Exceptions::Win32APIError, "Unable to open the Certificate Store `#{store_name}` with error: #{last_error}." end - certstore_handle + certstore_handler end def close - closed = CertCloseStore(@certstore_handle, CERT_CLOSE_STORE_FORCE_FLAG) + closed = CertCloseStore(@certstore_handler, CERT_CLOSE_STORE_FORCE_FLAG) unless closed last_error = FFI::LastError.error raise Chef::Exceptions::Win32APIError, "Unable to close the Certificate Store with error: #{last_error}." diff --git a/lib/win32/certstore/store_base.rb b/lib/win32/certstore/store_base.rb index c5653d9..02ac78d 100644 --- a/lib/win32/certstore/store_base.rb +++ b/lib/win32/certstore/store_base.rb @@ -15,48 +15,79 @@ # See the License for the specific language governing permissions and # limitations under the License. -require 'store/crypto' +require 'mixin/crypto' +require 'openssl' module Win32 class Certstore module StoreBase - include Win32::Store::Crypto + include Win32::Mixin::Crypto + include Win32::Mixin::Assertions include Chef::Mixin::WideString + include Chef::Mixin::ShellOut - def cert_list(certstore_handle) + def cert_list(store_handler) cert_name = FFI::MemoryPointer.new(2, 128) cert_list = [] begin - while (pCertContext = CertEnumCertificatesInStore(certstore_handle, pCertContext) and not pCertContext.null? ) do + while (pCertContext = CertEnumCertificatesInStore(store_handler, pCertContext) and not pCertContext.null? ) do if (CertGetNameStringW(pCertContext, CERT_NAME_FRIENDLY_DISPLAY_TYPE, CERT_NAME_ISSUER_FLAG, nil, cert_name, 1024)) cert_list << cert_name.read_wstring end end CertFreeCertificateContext(pCertContext) rescue Exception => e - lookup_error + lookup_error("list") end cert_list.to_json end + + def cert_add(store_handler, cert_file_path) + validate_certificate(cert_file_path) + file_content = read_certificate_content(cert_file_path) + pointer_cert = FFI::MemoryPointer.from_string(file_content) + cert_length = file_content.bytesize + begin + if (CertAddEncodedCertificateToStore(store_handler, X509_ASN_ENCODING, pointer_cert, cert_length, 2, nil)) + "Added certificate #{File.basename(cert_file_path)} successfully" + else + lookup_error + end + rescue Exception => e + lookup_error("add") + end + end private - def lookup_error + def lookup_error(failed_operation = nil) last_error = FFI::LastError.error case last_error when 1223 - raise Chef::Exceptions::Win32APIError, "The operation was canceled by the user. " + raise Chef::Exceptions::Win32APIError, "The operation was canceled by the user." when -2146885628 raise Chef::Exceptions::Win32APIError, "Cannot find object or property." when -2146885629 - raise Chef::Exceptions::Win32APIError, "An error occurred while reading or writing to a file. " + raise Chef::Exceptions::Win32APIError, "An error occurred while reading or writing to a file." when -2146881269 raise Chef::Exceptions::Win32APIError, "ASN1 bad tag value met. -- Is the certificate in DER format?" when -2146881278 - raise Chef::Exceptions::Win32APIError, "ASN1 unexpected end of data. " + raise Chef::Exceptions::Win32APIError, "ASN1 unexpected end of data." else - raise Chef::Exceptions::Win32APIError, "Unable to load certificate with error: #{last_error}." + raise Chef::Exceptions::Win32APIError, "Unable to #{failed_operation} certificate with error: #{last_error}." + end + end + + # This is a single public certificate in X509 DER format. + # If your certificate has a header and footer line like "---- BEGIN CERTIFICATE ----" then it is in PEM format, not DER format. + # A certificate can be converted with `openssl x509 -in example.crt -out example.der -outform DER` + def read_certificate_content(cert_path) + unless (File.extname(cert_path) == ".der") + temp_file = shell_out("powershell.exe -Command $env:temp").stdout.strip.concat("\\TempCert.der") + shell_out("powershell.exe -Command openssl x509 -in #{cert_path} -outform DER -out #{temp_file}") + cert_path = temp_file end + File.read("#{cert_path}") end end diff --git a/lib/win32/store/assertions.rb b/lib/win32/mixin/assertions.rb similarity index 78% rename from lib/win32/store/assertions.rb rename to lib/win32/mixin/assertions.rb index 5209618..3662951 100644 --- a/lib/win32/store/assertions.rb +++ b/lib/win32/mixin/assertions.rb @@ -15,12 +15,19 @@ # See the License for the specific language governing permissions and # limitations under the License. -module Win32::Store::Assertions +module Win32::Mixin::Assertions # Validate certificate store name def validate_store(store_name) unless valid_store_name.include?(store_name&.upcase) - raise ArgumentError, "Invalid Certificate Store. Valid certificate store name are: #{valid_store_name}" + raise ArgumentError, "Invalid Certificate Store." + end + end + + # Validate certificate type + def validate_certificate(cert_file_path) + unless (!cert_file_path.nil? && File.extname(cert_file_path) =~ /.cer|.crt|.pfx|.der/ ) + raise ArgumentError, "Invalid Certificate format." end end diff --git a/lib/win32/store/crypto.rb b/lib/win32/mixin/crypto.rb similarity index 82% rename from lib/win32/store/crypto.rb rename to lib/win32/mixin/crypto.rb index 0ffc74a..477ebbf 100644 --- a/lib/win32/store/crypto.rb +++ b/lib/win32/mixin/crypto.rb @@ -22,7 +22,7 @@ require 'mixlib/shellout' module Win32 - module Store + module Mixin module Crypto extend Chef::ReservedNames::Win32::API extend FFI::Library @@ -69,11 +69,15 @@ module Crypto BYTE = FFI::TypeDefs[:pointer] class CERT_CONTEXT < FFI::Struct - layout :hCertStore, :HANDLE, - :dwCertEncodingType, :DWORD, - :pbCertEncoded, :PWSTR, - :cbCertEncoded, :DWORD, - :pCertInfo, :pointer + layout :cbElement, :DWORD, + :pbElement, :pointer + def initialize(str = nil) + super(nil) + if str + self[:pbElement] = FFI::MemoryPointer.from_string(str) + self[:cbElement] = str.bytesize + end + end end ############################################################################### @@ -94,6 +98,12 @@ class CERT_CONTEXT < FFI::Struct safe_attach_function :CertEnumCertificateContextProperties, [PCCERT_CONTEXT, :DWORD], :DWORD # Clean up safe_attach_function :CertFreeCertificateContext, [PCCERT_CONTEXT], :BOOL + # Add certificate file in certificate store. + safe_attach_function :CertAddSerializedElementToStore, [HCERTSTORE, :pointer, :DWORD, :DWORD, :DWORD, :DWORD, :LMSTR, :LPVOID], :BOOL + # Add certification to certification store - Ref: https://msdn.microsoft.com/en-us/library/windows/desktop/aa376015(v=vs.85).aspx + safe_attach_function :CertAddEncodedCertificateToStore, [HCERTSTORE, :DWORD, :PWSTR, :DWORD, :INT_PTR, PCCERT_CONTEXT], :BOOL + + safe_attach_function :CertSerializeCertificateStoreElement, [PCCERT_CONTEXT, :DWORD, :pointer, :DWORD], :BOOL end end end diff --git a/spec/win32/unit/assets/example-cert.der b/spec/win32/unit/assets/example-cert.der new file mode 100644 index 0000000..87e554c Binary files /dev/null and b/spec/win32/unit/assets/example-cert.der differ diff --git a/spec/win32/unit/certstore/certstore_spec.rb b/spec/win32/unit/certstore/certstore_spec.rb index e551ba5..b0c8977 100644 --- a/spec/win32/unit/certstore/certstore_spec.rb +++ b/spec/win32/unit/certstore/certstore_spec.rb @@ -21,26 +21,27 @@ describe Win32::Certstore do let (:certstore) { Win32::Certstore } + let (:certbase) { Win32::Certstore::StoreBase } describe "#list" do context "When passing empty certificate store name" do let (:store_name) { "" } - it "Raise ArgumentError" do - expect { certstore.open(store_name) }.to raise_error(ArgumentError) + it "raises ArgumentError" do + expect { certstore.open(store_name) }.to raise_error("Invalid Certificate Store.") end end context "When passing invalid certificate store name" do let (:store_name) { "Chef" } - it "Raise ArgumentError" do - expect { certstore.open(store_name) }.to raise_error(ArgumentError) + it "raises ArgumentError" do + expect { certstore.open(store_name) }.to raise_error("Invalid Certificate Store.") end end context "When passing empty certificate store name" do let (:store_name) { nil } - it "Raise ArgumentError" do - expect { certstore.open(store_name) }.to raise_error(ArgumentError) + it "raises ArgumentError" do + expect { certstore.open(store_name) }.to raise_error("Invalid Certificate Store.") end end @@ -48,9 +49,9 @@ let (:store_name) { "root" } let (:root_certificate_name) { "Microsoft Root Certificate Authority"} before(:each) do - allow_any_instance_of(Win32::Certstore::StoreBase).to receive(:cert_list).and_return([root_certificate_name]) + allow_any_instance_of(certbase).to receive(:cert_list).and_return([root_certificate_name]) end - it "return certificate list" do + it "returns certificate list" do store = certstore.open(store_name) certificate_list = store.list expect(certificate_list.size).to eql(1) @@ -58,15 +59,53 @@ end end - context "When passing valid certificate store name" do + context "When adding invalid certificate" do let (:store_name) { "root" } - before(:each) do - allow_any_instance_of(Win32::Certstore::StoreBase).to receive(:cert_list).and_return([]) + let (:cert_file_path) { '.\win32\unit\assets\test.cer' } + it "returns no certificate list" do + allow(certbase).to receive(:CertAddEncodedCertificateToStore).and_return(false) + store = certstore.open(store_name) + expect { store.add(cert_file_path) }.to raise_error(Chef::Exceptions::Win32APIError) + end + end + + context "When adding certificate failed with FFI::LastError" do + let (:store_name) { "root" } + let (:cert_file_path) { '.\win32\unit\assets\test.cer' } + + it "returns 'The operation was canceled by the user'" do + allow(certbase).to receive(:CertAddEncodedCertificateToStore).and_return(false) + allow(FFI::LastError).to receive(:error).and_return(1223) + store = certstore.open(store_name) + expect { store.add(cert_file_path) }.to raise_error("The operation was canceled by the user.") end - it "return no certificate list" do + + it "returns 'Cannot find object or property'" do + allow(certbase).to receive(:CertAddEncodedCertificateToStore).and_return(false) + allow(FFI::LastError).to receive(:error).and_return(-2146885628) store = certstore.open(store_name) - certificate_list = store.list - expect(certificate_list.size).to eql(0) + expect { store.add(cert_file_path) }.to raise_error("Cannot find object or property.") + end + + it "returns 'An error occurred while reading or writing to a file'" do + allow(certbase).to receive(:CertAddEncodedCertificateToStore).and_return(false) + allow(FFI::LastError).to receive(:error).and_return(-2146885629) + store = certstore.open(store_name) + expect { store.add(cert_file_path) }.to raise_error("An error occurred while reading or writing to a file.") + end + + it "returns 'ASN1 bad tag value met. -- Is the certificate in DER format?'" do + allow(certbase).to receive(:CertAddEncodedCertificateToStore).and_return(false) + allow(FFI::LastError).to receive(:error).and_return(-2146881269) + store = certstore.open(store_name) + expect { store.add(cert_file_path) }.to raise_error("ASN1 bad tag value met. -- Is the certificate in DER format?") + end + + it "returns 'ASN1 unexpected end of data'" do + allow(certbase).to receive(:CertAddEncodedCertificateToStore).and_return(false) + allow(FFI::LastError).to receive(:error).and_return(-2146881278) + store = certstore.open(store_name) + expect { store.add(cert_file_path) }.to raise_error("ASN1 unexpected end of data.") end end end diff --git a/spec/win32/unit/store/assertions_spec.rb b/spec/win32/unit/store/assertions_spec.rb index aa5a85a..f12bb76 100644 --- a/spec/win32/unit/store/assertions_spec.rb +++ b/spec/win32/unit/store/assertions_spec.rb @@ -18,10 +18,10 @@ require 'spec_helper' -describe Win32::Store::Assertions do +describe Win32::Mixin::Assertions do class Store - include Win32::Store::Assertions + include Win32::Mixin::Assertions end let (:certstore) { Store.new } @@ -29,30 +29,60 @@ class Store describe "#validate_store" do context "When passing empty certificate store name" do let (:store_name) { "" } - it "Raise ArgumentError" do - expect { certstore.validate_store(store_name) }.to raise_error(ArgumentError) + it "raises ArgumentError" do + expect { certstore.validate_store(store_name) }.to raise_error("Invalid Certificate Store.") end end context "When passing invalid certificate store name" do let (:store_name) { "Chef" } - it "Raise ArgumentError" do - expect { certstore.validate_store(store_name) }.to raise_error(ArgumentError) + it "raises ArgumentError" do + expect { certstore.validate_store(store_name) }.to raise_error("Invalid Certificate Store.") end end context "When passing empty certificate store name" do let (:store_name) { nil } - it "Raise ArgumentError" do - expect { certstore.validate_store(store_name) }.to raise_error(ArgumentError) + it "raises ArgumentError" do + expect { certstore.validate_store(store_name) }.to raise_error("Invalid Certificate Store.") end end context "When passing valid certificate store name" do let (:store_name) { "root" } - it "Not Raise ArgumentError" do + it "does not raise ArgumentError" do expect { certstore.validate_store(store_name) }.not_to raise_error(ArgumentError) end end end + + describe "#validate_certificate" do + context "When not passing certificate file" do + let (:cert_file_path) { "" } + it "raises ArgumentError" do + expect { certstore.validate_certificate(cert_file_path) }.to raise_error("Invalid Certificate format.") + end + end + + context "When passing invalid certificate" do + let (:cert_file_path) { "Chef" } + it "raises ArgumentError" do + expect { certstore.validate_certificate(cert_file_path) }.to raise_error("Invalid Certificate format.") + end + end + + context "When passing nil" do + let (:cert_file_path) { nil } + it "raises ArgumentError" do + expect { certstore.validate_certificate(cert_file_path) }.to raise_error("Invalid Certificate format.") + end + end + + context "When passing valid certificate file" do + let (:cert_file_path) { '.\win32\unit\assets\test.der' } + it "does not raise ArgumentError" do + expect { certstore.validate_certificate(cert_file_path) }.not_to raise_error(ArgumentError) + end + end + end end