Skip to content

Commit

Permalink
Add certificate in certificate store
Browse files Browse the repository at this point in the history
Signed-off-by: piyushawasthi <[email protected]>
  • Loading branch information
piyushawasthi committed Sep 26, 2017
1 parent 5ac4928 commit 3dda9f7
Show file tree
Hide file tree
Showing 9 changed files with 203 additions and 60 deletions.
3 changes: 1 addition & 2 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -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'
18 changes: 17 additions & 1 deletion lib/win32-certstore.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
$LOAD_PATH.unshift File.expand_path('../../lib/win32', __FILE__)
#
# Author:: Nimisha Sharad (<[email protected]>)
# 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"
43 changes: 27 additions & 16 deletions lib/win32/certstore/certstore.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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}."
Expand Down
51 changes: 41 additions & 10 deletions lib/win32/certstore/store_base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 9 additions & 2 deletions lib/win32/store/assertions.rb → lib/win32/mixin/assertions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
22 changes: 16 additions & 6 deletions lib/win32/store/crypto.rb → lib/win32/mixin/crypto.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
require 'mixlib/shellout'

module Win32
module Store
module Mixin
module Crypto
extend Chef::ReservedNames::Win32::API
extend FFI::Library
Expand Down Expand Up @@ -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

###############################################################################
Expand All @@ -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
Binary file added spec/win32/unit/assets/example-cert.der
Binary file not shown.
67 changes: 53 additions & 14 deletions spec/win32/unit/certstore/certstore_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,52 +21,91 @@
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

context "When passing valid certificate store name" do
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)
expect(certificate_list.first).to eql root_certificate_name
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
Expand Down
Loading

0 comments on commit 3dda9f7

Please sign in to comment.