-
Notifications
You must be signed in to change notification settings - Fork 6
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
[MSYS-543] [MSYS-546] Added feature to list certificates and add new certificate. #4
Conversation
a04049f
to
eee4766
Compare
690c4ab
to
cc77d44
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lib/win32/backend/core_ext.rb
Outdated
@@ -0,0 +1,83 @@ | |||
# |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @mwrock
lib/win32/backend/policies.rb
Outdated
@@ -0,0 +1,44 @@ | |||
# |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe assertions?
lib/win32/backend/policies.rb
Outdated
module Win32::Backend::Policies | ||
|
||
# Validate import certificate file format | ||
def validate_args(import_cert_params) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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}'" |
There was a problem hiding this comment.
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
.
lib/win32/certstore/certstore.rb
Outdated
|
||
def open store_name |
There was a problem hiding this comment.
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.
lib/win32/certstore/certstore.rb
Outdated
end | ||
certstore_handle | ||
def self.add_cert(*args) | ||
Win32::Certstore::Certificate::Add.new(args) |
There was a problem hiding this comment.
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.
lib/win32/certstore/certstore.rb
Outdated
end | ||
closed | ||
def self.list_cert(certstore_name) | ||
Win32::Certstore::Certificate.new.list(certstore_name) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
We should add the test coverage for the newly added files too e.g. |
94f0826
to
1da21be
Compare
lib/win32/certstore/certstore.rb
Outdated
|
||
def open store_name | ||
def self.open_store(store_name) |
There was a problem hiding this comment.
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:
- All the methods defined inside
Certstore
are class methods. Does this bind us to define only class methods inCertstore
in future? - After renaming the method user will call
Win32::Certificate.open_store(certstore_name)
. It would look better to callWin32::Certificate.open(certstore_name)
. Here if it's a Class method, it won't override Ruby'sopen
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.
There was a problem hiding this comment.
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.
Thanks for quickly working on the review comments @piyushawasthi . Overall very good work! |
lib/win32/certstore/certstore.rb
Outdated
|
||
def open store_name | ||
def self.open_store(store_name) |
There was a problem hiding this comment.
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.
Signed-off-by: piyushawasthi <[email protected]>
1da21be
to
deea2dc
Compare
@mwrock , currently
? |
That's correct. Ideally we would not want a user of this API to have to deal with handles directly. |
@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) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
lib/win32/certstore/certstore.rb
Outdated
|
||
def open store_name |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
@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. |
@NimishaS - Ya, That's work, We'll do pairing for better design. |
Signed-off-by: piyushawasthi <[email protected]>
1b2be0f
to
5ac4928
Compare
|
||
def open(store_name) | ||
certstore_handler = CertOpenSystemStoreW(nil, wstring(store_name)) | ||
unless certstore_handler | ||
last_error = FFI::LastError.error |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense.
lib/win32/certstore/certstore.rb
Outdated
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same above
lib/win32/certstore/store_base.rb
Outdated
end | ||
CertFreeCertificateContext(pCertContext) | ||
rescue Exception => e | ||
@error = "load" |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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.
lib/win32/certstore/store_base.rb
Outdated
begin | ||
if (CertAddEncodedCertificateToStore(store_handler, X509_ASN_ENCODING, pointer_cert, cert_length, 2, nil)) | ||
return "Added certificate #{File.basename(cert_path)} successfully" | ||
else |
There was a problem hiding this comment.
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?
lib/win32/certstore/store_base.rb
Outdated
when -2146881278 | ||
raise Chef::Exceptions::Win32APIError, "ASN1 unexpected end of data. " | ||
else | ||
raise Chef::Exceptions::Win32APIError, "Unable to #{@error} certificate with error: #{last_error}." |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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
a939cf3
to
a60258e
Compare
Signed-off-by: piyushawasthi <[email protected]>
a60258e
to
3dda9f7
Compare
How to use win32-certstore. 1: Listing certificates of root certificate store.
or
2: Adding certificate to root certificate store.
or
|
can you but the above examples in a What's the thinking behind the |
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 |
e2550d3
to
64213a7
Compare
Signed-off-by: piyushawasthi <[email protected]>
64213a7
to
393454c
Compare
Signed-off-by: piyushawasthi <[email protected]>
@@ -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" |
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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:
- use the temp directory without having to shell out to get it (using
ENV['TEMP']
in Ruby would have been more appropriate here) - 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 |
There was a problem hiding this comment.
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.
I made some changes and merged this via #6. |
thanks @btm |
This PR have following feature.
1: Adding certificate in certificate store.
2: Listing certificates from any certificate store.
Signed-off-by: piyushawasthi [email protected]