Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MSYS-543] [MSYS-546] Added feature to list certificates and add new certificate. #4

Merged
merged 5 commits into from
Nov 15, 2017

Conversation

piyushawasthi
Copy link
Contributor

@piyushawasthi piyushawasthi commented Aug 17, 2017

This PR have following feature.
1: Adding certificate in certificate store.
2: Listing certificates from any certificate store.

Signed-off-by: piyushawasthi [email protected]

@piyushawasthi piyushawasthi requested review from NimishaS and btm August 17, 2017 09:53
@piyushawasthi piyushawasthi force-pushed the piyush/single_file_cert_list branch from a04049f to eee4766 Compare August 17, 2017 10:17
@piyushawasthi piyushawasthi changed the title One file to store all the certificates function Certificates List Aug 17, 2017
@piyushawasthi piyushawasthi force-pushed the piyush/single_file_cert_list branch 3 times, most recently from 690c4ab to cc77d44 Compare August 17, 2017 12:16
Copy link

@NimishaS NimishaS left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@btm, @mwrock I have suggested some design related changes here. Please share your views on them.

@@ -0,0 +1,83 @@
#

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use some other name for this folder than backend? Since we don't have an architecture of frontend and backend, this name looks a bit confusing.
I would also prefer if the names of files match with that of Chef's. E.g. Chef has crypto.rb file for defining the cryptography windows functions: https://github.com/chef/chef/blob/master/lib/chef/win32/api/crypto.rb

@btm, @mwrock , please share your views.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree there are better names than backend. Maybe "store"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @mwrock

@@ -0,0 +1,44 @@
#

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file contains methods for validation. @btm, @mwrock , please share your views if the file name policies sounds good to you.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe assertions?

module Win32::Backend::Policies

# Validate import certificate file format
def validate_args(import_cert_params)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now this method is not being used, but I suppose this will be used while certificate add functionality. Since this validates the certificate, would it be good to name it as validate_certificate as we have validate_store below?

return certificates_list
end

def list_cert
Copy link

@NimishaS NimishaS Aug 23, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would probably be a good idea to keep all the functions calling Windows functions in a separate file which could be kept in the folder currently named as backend.
That way we'll have only functions like list, add, delete, verify etc defined for the Certificate class and all the Windows function related definition and implementation will be in a separate folder.

@btm, @mwrock , please comment.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agree and especially if you rename backend to store, I'd expect it to expose list

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay

include Certificate::Win32Base

def list(store_name)
@store_name = store_name

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use store_name directly here instead of reassigning it to @store_name?

def get_PScommand(user_parms)
user_parms = update_params(user_parms)
if user_parms.last =~ /.pfx/
cmd = "powershell.exe -Command certutil -addstore -f -importpfx '#{user_parms.first}' '#{user_parms.last}'"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We aren't using certutil powershell utility for listing certificate. This function is also not being called. We can probably remove those functions which are not related to listing certificates as we aren't sure if we'll be using certutil.


def open store_name

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should have open and close methods in this file for Certstore instead of win32_base file, probably with different names if open and close are too general.

end
certstore_handle
def self.add_cert(*args)
Win32::Certstore::Certificate::Add.new(args)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this PR is for only list_cert, please remove all the methods that aren't related to this functionality.

end
closed
def self.list_cert(certstore_name)
Win32::Certstore::Certificate.new.list(certstore_name)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be a good practice to have an initialize method for this class where we can have an instance variable defined for Win32::Certstore::Certificate.new. Otherwise we'll have to call Win32::Certstore::Certificate.new multiple times for different functions like add_cert, delete_cert etc.

expect(Win32::Certstore::VERSION).not_to be nil
end

describe "#open" do

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please update the specs for certstore as per the code changes. They shouldn't be commented out.

@NimishaS NimishaS requested a review from mwrock August 23, 2017 12:44
@NimishaS
Copy link

We should add the test coverage for the newly added files too e.g. win32_api, policies etc.

@piyushawasthi piyushawasthi force-pushed the piyush/single_file_cert_list branch 6 times, most recently from 94f0826 to 1da21be Compare August 24, 2017 13:00

def open store_name
def self.open_store(store_name)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of the changes look good to me. Just 2 doubts:

  1. All the methods defined inside Certstore are class methods. Does this bind us to define only class methods in Certstore in future?
  2. After renaming the method user will call Win32::Certificate.open_store(certstore_name). It would look better to call Win32::Certificate.open(certstore_name). Here if it's a Class method, it won't override Ruby's open method since this will always be called along with the Class name.

@btm, @mwrock please suggest if this looks fine or some modifications should be done.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a class method of open could instantiate an instance of store and return it. One would call list and other instance methods of of that. open could also take a block so you could have:

Win32::Certstore.open("MY") do |store| 
    store.list
end

If called with a block, you could call close on the store. It might also be worth closing the store in a finalizer.

@NimishaS
Copy link

NimishaS commented Aug 24, 2017

Thanks for quickly working on the review comments @piyushawasthi . Overall very good work!
I have just few doubts which I shared above. Please share a sample code to show how the functions for certstore and certificate will be called along with a sample output.
This will help in understanding the usage. @btm, @mwrock please share your views after @piyushawasthi 's comment.


def open store_name
def self.open_store(store_name)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a class method of open could instantiate an instance of store and return it. One would call list and other instance methods of of that. open could also take a block so you could have:

Win32::Certstore.open("MY") do |store| 
    store.list
end

If called with a block, you could call close on the store. It might also be worth closing the store in a finalizer.

@piyushawasthi piyushawasthi force-pushed the piyush/single_file_cert_list branch from 1da21be to deea2dc Compare August 28, 2017 05:58
@NimishaS
Copy link

@mwrock , currently Win32::Certstore.open uses CertOpenSystemStoreW and returns a handler to the certificate. According to your #4 (comment), it should return an object of CertStore.
Are you suggesting to modify the definition of Win32::Certstore.list_cert with:

Win32::Certstore.open("MY") do |store| 
    store.list
end

?

@mwrock
Copy link
Member

mwrock commented Aug 28, 2017

That's correct. Ideally we would not want a user of this API to have to deal with handles directly.

@piyushawasthi
Copy link
Contributor Author

@mwrock - Made changes as per your comment please review also if you get chance please review directory structure and list certificate calling way details here: https://gist.github.com/piyushawasthi/76fad63c0fe8d6cd3cc39cda94ada505

include Chef::Mixin::ShellOut
include Chef::Mixin::WideString

def list_cert(store_handle)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think list_cert belongs here. Since this is the Certificate class I would expect this to represent a single certificate instance. I'd expect to list certs using a CertStore

# To Display Certificate List
# Take input certificate store name
# Return List in JSON format
def list
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see note above. I'd expect to use the certificate store to list certs and not a Certificate class. Since this is the only method here, I wonder if we need this class at all.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mwrock , yeah list may be moved to CertStore since it doesn't represent a single certificate instance. But there will be other methods for adding, deleting, verifying certificates, etc which will make sense in this class. Please correct me if I missing something.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think methods like adding and deleting certs from a store belong in the store and would take a cert instance.


def open store_name
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think open and close should remain in certstore. Open should be a class method that returns (or yields) a store.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I too agree that open and close methods should be in certstore. But @mwrock, if open will be a class method that returns a certstore object, then that object will contain the certificate handler and users will have access to it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the handle can be hidden as a private member. Alternatively you could hide open and close completely. Internally you would have public store operations like list check for a handle and call a private instance open if there isn't one. For closing, you could use ObjectSpace.define_finalizer so that it is automatically freed when it is GC'd. This is the pattern I used in winrm and it worked well: https://github.com/WinRb/WinRM/blob/master/lib/winrm/shells/base.rb#L171

@NimishaS
Copy link

NimishaS commented Sep 1, 2017

@piyushawasthi , it would be helpful if we work on a design first taking @mwrock's comments into consideration. We should elaborate the file structure along with the methods (class or instance) that the file contains. We should highlight the Classes and Modules too. Once @mwrock or @btm approves it then we can go ahead with the implementation.

@piyushawasthi
Copy link
Contributor Author

@NimishaS - Ya, That's work, We'll do pairing for better design.

@piyushawasthi piyushawasthi force-pushed the piyush/single_file_cert_list branch from 1b2be0f to 5ac4928 Compare September 4, 2017 09:19

def open(store_name)
certstore_handler = CertOpenSystemStoreW(nil, wstring(store_name))
unless certstore_handler
last_error = FFI::LastError.error

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use lookup_error method here as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

open and close are the private methods of certstore.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense.

def close certstore_handle
closed = CertCloseStore(certstore_handle, CERT_CLOSE_STORE_FORCE_FLAG)
def close
closed = CertCloseStore(@certstore_handler, CERT_CLOSE_STORE_FORCE_FLAG)
unless closed
last_error = FFI::LastError.error

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use lookup_error method here as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same above

end
CertFreeCertificateContext(pCertContext)
rescue Exception => e
@error = "load"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since @error actually holds the operation which fails here, it would be good to name this variable as @failed_operation. Also it seems like the operation is list in this case. Is there any reason to use load?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest to pass failed_operation as an argument to lookup_error method with default value as an empty string. By doing so someone can easily understand by looking at the method ( e.g. lookup_error(failed_operation = '') ) that the function accepts operation too.
Right now one has to look at the complete definition of the method to understand that @error instance variable can also be passed.

begin
if (CertAddEncodedCertificateToStore(store_handler, X509_ASN_ENCODING, pointer_cert, cert_length, 2, nil))
return "Added certificate #{File.basename(cert_path)} successfully"
else

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this If-Else block for exception handling, since we are already using a rescue block?

when -2146881278
raise Chef::Exceptions::Win32APIError, "ASN1 unexpected end of data. "
else
raise Chef::Exceptions::Win32APIError, "Unable to #{@error} certificate with error: #{last_error}."

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpicking... Although this message is absolutely correct, I have one suggestion. I would suggest to use Failed instead of Unable. The reason behind this is that when I am debugging a big log file, I search for keywords like error, fail etc. This highlights the issue very clearly. It's unlikely to search for unable keyword.

allow(certbase).to receive(:CertAddEncodedCertificateToStore).and_return(false)
allow(FFI::LastError).to receive(:error).and_return(-2146885628)
store = certstore.open(store_name)
expect { store.add(cert_file_path) }.to raise_error(Chef::Exceptions::Win32APIError)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be good to check this with the actual error message.

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(Chef::Exceptions::Win32APIError)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be good to check this with the actual error message

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(Chef::Exceptions::Win32APIError)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be good to check this with the actual error message

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(Chef::Exceptions::Win32APIError)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be good to check this with the actual error message

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)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are manually raising some error anywhere, we should check with the actual error message instead of ArgumentError

@piyushawasthi piyushawasthi force-pushed the piyush/single_file_cert_list branch 5 times, most recently from a939cf3 to a60258e Compare September 26, 2017 10:38
@piyushawasthi piyushawasthi force-pushed the piyush/single_file_cert_list branch from a60258e to 3dda9f7 Compare September 26, 2017 11:04
@piyushawasthi
Copy link
Contributor Author

How to use win32-certstore.

1: Listing certificates of root certificate store.

store = Win32::Certstore.open("Root")
store.list

or

Win32::Certstore.open("Root") do |store|
    store.list
end

2: Adding certificate to root certificate store.

store = Win32::Certstore.open("Root")
store.add(certificate_file_path)

or

Win32::Certstore.open("Root") do |store|
   store.add(certificate_file_path)
end

@piyushawasthi piyushawasthi requested review from btm and mwrock September 27, 2017 09:19
@btm
Copy link
Contributor

btm commented Sep 28, 2017

can you but the above examples in a README.md?

What's the thinking behind the lib/win32/win32-certstore/version.rb file pattern? why not lib/win32/certstore/version.rb

@mwrock
Copy link
Member

mwrock commented Sep 28, 2017

I'd consider using a finalizer for the certstore_handle. Unless you are using the store inside of a block, the store will remain open for the duration of the process. What happens if you try to open a new instance of the store without closing the other? If it throws an error related to it already being open, we should handle that and emit a clear message instructing the user to call close first on any other instances. I'd also add examples that call close in the readme.

@piyushawasthi piyushawasthi force-pushed the piyush/single_file_cert_list branch 4 times, most recently from e2550d3 to 64213a7 Compare October 9, 2017 13:21
Signed-off-by: piyushawasthi <[email protected]>
@piyushawasthi piyushawasthi force-pushed the piyush/single_file_cert_list branch from 64213a7 to 393454c Compare October 9, 2017 13:22
Signed-off-by: piyushawasthi <[email protected]>
@NimishaS
Copy link

NimishaS commented Nov 9, 2017

@mwrock , @btm , please review.

@piyushawasthi piyushawasthi changed the title Certificates List [MSYS-543] [MSYS-546] Added feature to listing certificates and add new certificate. Nov 9, 2017
@piyushawasthi piyushawasthi changed the title [MSYS-543] [MSYS-546] Added feature to listing certificates and add new certificate. [MSYS-543] [MSYS-546] Added feature to list certificates and add new certificate. Nov 9, 2017
@@ -15,4 +16,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.

require 'win32/certstore/certstore'
require "mixin/crypto"
require "certstore/certstore"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder why this is certstore/certstore here ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest being more specific here, i.e. use `require "win32/certstore/certstore" so we don't risk picking up something else in the LOAD_PATH called 'certstore'.

@@ -0,0 +1,6 @@
module Win32
module Win32Certstore
VERSION = "1.0.0"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please change this to 0.1.0 so we can make some pre-releases with non-prerelease versions. we'll ship 1.0.0 when we think we're feature complete.

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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this test and all tests after it fail on my Windows desktop when running 'rake spec' with an error like:

  1) Win32::Certstore#list When adding invalid certificate returns no certificate list
     Failure/Error: expect { store.add(cert_file_path) }.to raise_error(Chef::Exceptions::Win32APIError)

       expected Chef::Exceptions::Win32APIError, got #<Errno::ENOENT: No such file or directory @ rb_sysopen - C:\Users\btm_000\AppData\Local\Temp\TempCert.der> with backtrace:
         # C:/Users/btm_000/Documents/Github/win32-certstore/lib/win32/certstore/store_base.rb:90:in `read'
         # C:/Users/btm_000/Documents/Github/win32-certstore/lib/win32/certstore/store_base.rb:90:in `read_certificate_content'
         # C:/Users/btm_000/Documents/Github/win32-certstore/lib/win32/certstore/store_base.rb:47:in `cert_add'
         # ./lib/win32/certstore/certstore.rb:51:in `add'
         # ./spec/win32/unit/certstore/certstore_spec.rb:68:in `block (5 levels) in <top (required)>'
         # ./spec/win32/unit/certstore/certstore_spec.rb:68:in `block (4 levels) in <top (required)>'
     # ./spec/win32/unit/certstore/certstore_spec.rb:68:in `block (4 levels) in <top (required)>'

# 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")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use Tempfile.new here. This will:

  1. use the temp directory without having to shell out to get it (using ENV['TEMP'] in Ruby would have been more appropriate here)
  2. produce a temp file with a random string inserted so you don't have to worry about the existence of a file, or a security issue with someone maliciously creating a temporary file with that filename, etc.

require 'mixlib/shellout'

module Win32
module Mixin
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you move this to Win32::Crypto::Mixin please? one more directory. Nothing here is meant to be shared with other Win32 libraries so let's use our own namespace.

@btm btm mentioned this pull request Nov 15, 2017
@btm btm merged commit f30b3a4 into chef:master Nov 15, 2017
@btm
Copy link
Contributor

btm commented Nov 15, 2017

I made some changes and merged this via #6.

@piyushawasthi
Copy link
Contributor Author

thanks @btm

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants