Skip to content

Commit

Permalink
Merge pull request #79 from chef/jfm/jfm_updates
Browse files Browse the repository at this point in the history
Updated Certstore to correctly understand CurrentUser vs LocalMachine stores
  • Loading branch information
marcparadise authored Apr 15, 2021
2 parents 6f1c394 + e6f6d52 commit 949cf57
Show file tree
Hide file tree
Showing 6 changed files with 47 additions and 189 deletions.
20 changes: 4 additions & 16 deletions lib/win32/certstore.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,20 +74,8 @@ def add_pfx(path, password, key_properties = 0)
# Return `OpenSSL::X509` certificate object
# @param request [thumbprint<string>] of certificate
# @return [Object] of certificates in OpenSSL::X509 format
def get(certificate_thumbprint)
cert_get(certificate_thumbprint)
end

# Returns a filepath to a PKCS12 container. The filepath is in a temporary folder so normal housekeeping by the OS should clear it.
# However, you should delete it yourself anyway.
# @param certificate_thumbprint [String] Is the thumbprint of the pfx blob you want to capture
# @param store_location: [String] A location in the Cert store where the pfx is located, typically 'LocalMachine'
# @param export_password: [String] The password to export with. P12 objects are an encrypted container that have a private key in \
# them and a password is required to export them.
# @param output_path: [String] The path where the you want P12 exported to.
# @return [Object] of certificate set in PKSC12 format at the path specified above
def get_pfx(certificate_thumbprint, store_location: @store_location, export_password:, output_path: "")
get_cert_pfx(certificate_thumbprint, store_location: store_location, export_password: export_password, output_path: output_path)
def get(certificate_thumbprint, store_name: @store_name, store_location: @store_location)
cert_get(certificate_thumbprint, store_name: store_name, store_location: store_location)
end

# Returns all the certificates in a store
Expand All @@ -114,8 +102,8 @@ def search(search_token)
# Validates a certificate in a certificate store on the basis of time validity
# @param request[thumbprint<string>] of certificate
# @return [true, false] only true or false
def valid?(certificate_thumbprint)
cert_validate(certificate_thumbprint)
def valid?(certificate_thumbprint, store_location: "", store_name: "")
cert_validate(certificate_thumbprint, store_location: store_location, store_name: store_name)
end

# To close and destroy pointer of open certificate store handler
Expand Down
44 changes: 6 additions & 38 deletions lib/win32/certstore/mixin/helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,52 +21,20 @@ module Win32
class Certstore
module Mixin
module Helper
# PSCommand to search certificate from thumbprint and either turn it into a pem or return a path to a pfx object
def cert_ps_cmd(thumbprint, store_location: "LocalMachine", export_password: "1234", output_path: "")
def cert_ps_cmd(thumbprint, store_location: "LocalMachine", store_name: "My")
<<-EOH
$cert = Get-ChildItem Cert:\'#{store_location}' -Recurse | Where { $_.Thumbprint -eq '#{thumbprint}' }
$cert = Get-ChildItem Cert:\\#{store_location}\\#{store_name} -Recurse | Where { $_.Thumbprint -eq "#{thumbprint}" }
# The function and the code below test to see if a) the cert has a private key and b) it has a
# Enhanced Usage of Client Auth. Those 2 attributes would mean this is a pfx-able object
function test_cert_values{
$usagelist = ($cert).EnhancedKeyUsageList
foreach($use in $usagelist){
if($use.FriendlyName -like "Client Authentication" ){
return $true
}
}
return $false
}
$result = test_cert_values
$output_path = "#{output_path}"
if([string]::IsNullOrEmpty($output_path)){
$temproot = [System.IO.Path]::GetTempPath()
}
else{
$temproot = $output_path
}
if((($cert).HasPrivateKey) -and ($result -eq $true)){
$file_name = '#{thumbprint}'
$file_path = $(Join-Path -Path $temproot -ChildPath "$file_name.pfx")
$mypwd = ConvertTo-SecureString -String '#{export_password}' -Force -AsPlainText
$cert | Export-PfxCertificate -FilePath $file_path -Password $mypwd | Out-Null
$file_path
}
else {
$content = $null
if($cert -ne $null)
{
$content = $null
if($null -ne $cert)
{
$content = @(
'-----BEGIN CERTIFICATE-----'
[System.Convert]::ToBase64String($cert.RawData, 'InsertLineBreaks')
'-----END CERTIFICATE-----'
)
}
$content
}
$content
EOH
end

Expand Down
28 changes: 12 additions & 16 deletions lib/win32/certstore/mixin/string.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,12 @@ def utf8_to_wide(ustring)
ustring += "\000\000" if ustring.length == 0 || ustring[-1].chr != "\000"

# encode it all as UTF-16LE AKA Windows Wide Character AKA Windows Unicode
ustring = begin
if ustring.respond_to?(:encode)
ustring.encode("UTF-16LE")
else
require "iconv"
Iconv.conv("UTF-16LE", "UTF-8", ustring)
end
end
ustring = if ustring.respond_to?(:encode)
ustring.encode("UTF-16LE")
else
require "iconv"
Iconv.conv("UTF-16LE", "UTF-8", ustring)
end
ustring
end

Expand All @@ -53,14 +51,12 @@ def wide_to_utf8(wstring)
wstring = wstring.force_encoding("UTF-16LE") if wstring.respond_to?(:force_encoding)

# encode it all as UTF-8
wstring = begin
if wstring.respond_to?(:encode)
wstring.encode("UTF-8")
else
require "iconv"
Iconv.conv("UTF-8", "UTF-16LE", wstring)
end
end
wstring = if wstring.respond_to?(:encode)
wstring.encode("UTF-8")
else
require "iconv"
Iconv.conv("UTF-8", "UTF-16LE", wstring)
end
# remove trailing CRLF and NULL characters
wstring.strip!
wstring
Expand Down
27 changes: 10 additions & 17 deletions lib/win32/certstore/store_base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,15 @@ 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)
def cert_get(certificate_thumbprint, store_name:, store_location:)
validate_thumbprint(certificate_thumbprint)
thumbprint = update_thumbprint(certificate_thumbprint)
cert_pem = get_cert_pem(thumbprint)
cert_pem = get_cert_pem(thumbprint, store_name: store_name, store_location: store_location)
cert_pem = format_pem(cert_pem)
if cert_pem.empty?
raise ArgumentError, "Unable to retrieve the certificate"
end

unless cert_pem.empty?
build_openssl_obj(cert_pem)
end
Expand Down Expand Up @@ -138,10 +142,10 @@ 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)
def cert_validate(certificate_thumbprint, store_location:, store_name:)
validate_thumbprint(certificate_thumbprint)
thumbprint = update_thumbprint(certificate_thumbprint)
cert_pem = get_cert_pem(thumbprint)
cert_pem = get_cert_pem(thumbprint, store_name: store_name, store_location: store_location)
cert_pem = format_pem(cert_pem)
verify_certificate(cert_pem)
end
Expand Down Expand Up @@ -230,24 +234,13 @@ def der_cert(cert_obj)
end

# Get certificate pem
def get_cert_pem(thumbprint)
converted_store = if @store_location == CERT_SYSTEM_STORE_LOCAL_MACHINE
"LocalMachine"
else
"CurrentUser"
end
get_data = powershell_exec!(cert_ps_cmd(thumbprint, store_location: converted_store))
get_data.stdout
end

# Get PFX object
def get_cert_pfx(thumbprint, store_location: CERT_SYSTEM_STORE_LOCAL_MACHINE, export_password:, output_path: )
def get_cert_pem(thumbprint, store_name:, store_location:)
converted_store = if store_location == CERT_SYSTEM_STORE_LOCAL_MACHINE
"LocalMachine"
else
"CurrentUser"
end
get_data = powershell_exec!(cert_ps_cmd(thumbprint, export_password: export_password, store_location: converted_store, output_path: output_path))
get_data = powershell_exec!(cert_ps_cmd(thumbprint, store_location: converted_store, store_name: store_name))
get_data.stdout
end

Expand Down
36 changes: 8 additions & 28 deletions spec/win32/functional/win32/certstore_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,34 +34,14 @@
# passing valid thumbprint
it "returns the certificate_object if found" do
thumbprint = "b1bc968bd4f49d622aa89a81f2150152a41d829c"
expect(@store).to receive(:cert_get).with(thumbprint).and_return(cert_pem)
@store.get(thumbprint)
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")
end

# passing invalid thumbprint
it "returns nil if certificate not found" do
thumbprint = "b1bc968bd4f49d622aa89a81f2150152a41d829cab"
expect(@store).to receive(:cert_get).with(thumbprint).and_return(nil)
@store.get(thumbprint)
end
end

describe "#get_pfx" do
before { add_pfx }
let(:cert_pfx) { "d77803da081a5a556ab44c9cc74818767782c84b.pfx" }

# passing valid thumbprint
it "returns the certificate_object if found" do
thumbprint = "d77803da081a5a556ab44c9cc74818767782c84b"
out_put = @store.get_pfx(thumbprint, export_password: "1234")
file = ::File.basename(out_put.strip)
expect(file).to eq(cert_pfx)
end

# passing invalid thumbprint
it "returns nil if certificate not found" do
it "returns ArgumentError if certificate not found" do
thumbprint = "b1bc968bd4f49d622aa89a81f2150152a41d829cab"
expect(@store.get_pfx(thumbprint, export_password: "1234")).to eq("")
expect { @store.get(thumbprint, store_location: CERT_SYSTEM_STORE_CURRENT_USER, store_name: "My") }.to raise_error(ArgumentError)
end
end

Expand Down Expand Up @@ -109,15 +89,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).and_return(cert_pem)
@store.get(thumbprint)
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")
end

# passing invalid thumbprint
it "returns nil if certificate not found" do
thumbprint = "b1bc968bd4f49d622aa89a81f2150152a41d829cab"
expect(@store).to receive(:cert_get).with(thumbprint).and_return(nil)
@store.get(thumbprint)
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")
end
end

Expand Down
81 changes: 7 additions & 74 deletions spec/win32/unit/certstore_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -127,13 +127,13 @@
context "When passing invalid thumbprint" do
let(:store_name) { "root" }
let(:thumbprint) { "b1bc968bd4f49d622aa89a81f2150152a41d829c" }
let(:store_location) {}
before(:each) do
allow_any_instance_of(certbase).to receive(:get_cert_pem).and_return("")
end
it "returns nil" do
it "raises ArgumentError" do
store = certstore.open(store_name)
cert_obj = store.get(thumbprint)
expect(cert_obj).to eql(nil)
expect { store.get(thumbprint, store_location: CERT_SYSTEM_STORE_CURRENT_USER, store_name: store_name) }.to raise_error(ArgumentError, "Unable to retrieve the certificate")
end
end

Expand Down Expand Up @@ -188,70 +188,6 @@

CERT_SYSTEM_STORE_LOCAL_MACHINE = 0x00020000

describe "#get_cert_pfx" do
context "When passing invalid thumbprint" do
let(:store_name) { "root" }
let(:thumbprint) { "b1bc968bd4f49d622aa89a81f2150152a41d829c" }
let(:password) { "1234" }
before(:each) do
allow_any_instance_of(certbase).to receive(:get_cert_pfx).and_return("")
end
it "returns nil" do
store = certstore.open(store_name)
cert_obj = store.get_pfx(thumbprint, store_location: CERT_SYSTEM_STORE_LOCAL_MACHINE, export_password: password)
expect(cert_obj).to eql("")
end
end

context "When passing valid thumbprint" do
let(:store_name) { "root" }
let(:password) { "1234" }
let(:thumbprint) { "d77803da081a5a556ab44c9cc74818767782c84b" }
let(:cert_pfx) { File.binread('.\spec\win32\assets\steveb.pfx') }
before(:each) do
allow_any_instance_of(certbase).to receive(:get_cert_pfx).and_return(cert_pfx)
end
it "returns OpenSSL::PKCS12::Container Binary Object" do
store = certstore.open(store_name)
cert_obj = store.get_pfx(thumbprint, store_location: CERT_SYSTEM_STORE_LOCAL_MACHINE, export_password: password)
p12 = OpenSSL::PKCS12.new(cert_obj, password)
expect(p12).to be_an_instance_of(OpenSSL::PKCS12)
end
end

context "When passing valid thumbprint I get a private key back" do
let(:store_name) { "root" }
let(:password) { "1234" }
let(:thumbprint) { "d77803da081a5a556ab44c9cc74818767782c84b" }
let(:cert_pfx) { File.binread('.\spec\win32\assets\steveb.pfx') }
before(:each) do
allow_any_instance_of(certbase).to receive(:get_cert_pfx).and_return(cert_pfx)
end
it "returns OpenSSL::PKCS12 Private Key" do
store = certstore.open(store_name)
cert_obj = store.get_pfx(thumbprint, store_location: CERT_SYSTEM_STORE_LOCAL_MACHINE, export_password: password)
p12 = OpenSSL::PKCS12.new(cert_obj, password)
expect(p12.key).to be_an_instance_of(OpenSSL::PKey::RSA)
end
end

context "When passing valid thumbprint I get a certificate back" do
let(:store_name) { "root" }
let(:password) { "1234" }
let(:thumbprint) { "d77803da081a5a556ab44c9cc74818767782c84b" }
let(:cert_pfx) { File.binread('.\spec\win32\assets\steveb.pfx') }
before(:each) do
allow_any_instance_of(certbase).to receive(:get_cert_pfx).and_return(cert_pfx)
end
it "returns OpenSSL::PKCS12 Certificate" do
store = certstore.open(store_name)
cert_obj = store.get_pfx(thumbprint, store_location: CERT_SYSTEM_STORE_LOCAL_MACHINE, export_password: password, output_path: 'c:\foo\bar')
p12 = OpenSSL::PKCS12.new(cert_obj, password)
expect(p12.certificate).to be_an_instance_of(OpenSSL::X509::Certificate)
end
end
end

describe "#cert_delete" do
context "When passing empty certificate store name" do
let(:store_name) { "" }
Expand Down Expand Up @@ -323,10 +259,9 @@
before(:each) do
allow_any_instance_of(certbase).to receive(:get_cert_pem).and_return("")
end
it "returns nil" do
it "raises Error" do
store = certstore.open(store_name)
cert_obj = store.get(thumbprint)
expect(cert_obj).to eql(nil)
expect { store.get(thumbprint, store_location: CERT_SYSTEM_STORE_CURRENT_USER, store_name: store_name) }.to raise_error(ArgumentError, "Unable to retrieve the certificate")
end
end
end
Expand Down Expand Up @@ -664,8 +599,7 @@
end
it "returns nil" do
store = certstore.open(store_name, store_location: store_location)
cert_obj = store.get(thumbprint)
expect(cert_obj).to eql(nil)
expect { store.get(thumbprint, store_location: CERT_SYSTEM_STORE_CURRENT_USER, store_name: store_name) }.to raise_error(ArgumentError, "Unable to retrieve the certificate")
end
end

Expand Down Expand Up @@ -800,8 +734,7 @@
end
it "returns nil" do
store = certstore.open(store_name, store_location: store_location)
cert_obj = store.get(thumbprint)
expect(cert_obj).to eql(nil)
expect { store.get(thumbprint, store_location: CERT_SYSTEM_STORE_CURRENT_USER, store_name: store_name) }.to raise_error(ArgumentError, "Unable to retrieve the certificate")
end
end
end
Expand Down

0 comments on commit 949cf57

Please sign in to comment.