/
certificate_binding.rb
143 lines (123 loc) · 4.96 KB
/
certificate_binding.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
#
# Author:: Richard Lavey (richard.lavey@calastone.com)
# Cookbook:: windows
# Resource:: certificate_binding
#
# Copyright:: 2015-2017, Calastone Ltd.
# Copyright:: 2018, Chef Software, Inc.
#
# 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.
#
unified_mode true if respond_to?(:unified_mode)
property :cert_name, String, name_property: true
property :name_kind, Symbol, equal_to: [:hash, :subject], default: :subject
property :address, String, default: '0.0.0.0'
property :port, Integer, default: 443
property :app_id, String, default: '{4dc3e181-e14b-4a21-b022-59fc669b0914}'
property :store_name, String, default: 'MY', equal_to: ['TRUSTEDPUBLISHER', 'CLIENTAUTHISSUER', 'REMOTE DESKTOP', 'ROOT', 'TRUSTEDDEVICES', 'WEBHOSTING', 'CA', 'AUTHROOT', 'TRUSTEDPEOPLE', 'MY', 'SMARTCARDROOT', 'TRUST']
property :exists, [true, false]
load_current_value do |desired|
cmd = shell_out("#{netsh_command} http show sslcert #{address_mode(desired.address)}=#{desired.address}:#{desired.port}")
Chef::Log.debug "netsh reports: #{cmd.stdout}"
address desired.address
port desired.port
store_name desired.store_name
app_id desired.app_id
if cmd.exitstatus == 0
m = cmd.stdout.scan(/Certificate Hash\s+:\s?([A-Fa-f0-9]{40})/)
raise "Failed to extract hash from command output #{cmd.stdout}" if m.empty?
cert_name m[0][0]
name_kind :hash
exists true
else
exists false
end
end
def address_mode(address)
address.match(/(\d+\.){3}\d+|\[.+\]/).nil? ? 'hostnameport' : 'ipport'
end
def netsh_command
# account for Window's wacky File System Redirector
# http://msdn.microsoft.com/en-us/library/aa384187(v=vs.85).aspx
# especially important for 32-bit processes (like Ruby) on a
# 64-bit instance of Windows.
if ::File.exist?("#{ENV["WINDIR"]}\\sysnative\\netsh.exe")
"#{ENV["WINDIR"]}\\sysnative\\netsh.exe"
elsif ::File.exist?("#{ENV["WINDIR"]}\\system32\\netsh.exe")
"#{ENV["WINDIR"]}\\system32\\netsh.exe"
else
"netsh.exe"
end
end
action :create do
hash = new_resource.name_kind == :subject ? hash_from_subject : new_resource.cert_name
if current_resource.exists
needs_change = (hash.casecmp(current_resource.cert_name) != 0)
if needs_change
converge_by("Changing #{current_resource.address}:#{current_resource.port}") do
delete_binding
add_binding hash
end
else
Chef::Log.debug("#{new_resource.address}:#{new_resource.port} already bound to #{hash} - nothing to do")
end
else
converge_by("Binding #{new_resource.address}:#{new_resource.port}") do
add_binding hash
end
end
end
action :delete do
if current_resource.exists
converge_by("Deleting #{current_resource.address}:#{current_resource.port}") do
delete_binding
end
else
Chef::Log.debug("#{current_resource.address}:#{current_resource.port} not bound - nothing to do")
end
end
action_class do
def add_binding(hash)
cmd = "#{netsh_command} http add sslcert"
mode = address_mode(current_resource.address)
cmd << " #{mode}=#{current_resource.address}:#{current_resource.port}"
cmd << " certhash=#{hash}"
cmd << " appid=\"#{current_resource.app_id}\""
cmd << " certstorename=#{current_resource.store_name}"
check_hash hash
shell_out!(cmd)
end
def delete_binding
mode = address_mode(current_resource.address)
shell_out!("#{netsh_command} http delete sslcert #{mode}=#{current_resource.address}:#{current_resource.port}")
end
def check_hash(hash)
p = powershell_out!("Test-Path \"cert:\\LocalMachine\\#{current_resource.store_name}\\#{hash}\"")
unless p.stderr.empty? && p.stdout =~ /True/i
raise "A Cert with hash of #{hash} doesn't exist in keystore LocalMachine\\#{current_resource.store_name}"
end
nil
end
def hash_from_subject
# escape wildcard subject name (*.acme.com)
subject = new_resource.cert_name.sub(/\*/, '`*')
ps_script = "& { gci cert:\\localmachine\\#{new_resource.store_name} | where { $_.subject -like '*#{subject}*' } | select -first 1 -expandproperty Thumbprint }"
Chef::Log.debug "Running PS script #{ps_script}"
p = powershell_out!(ps_script)
raise "#{ps_script} failed with #{p.stderr}" if p.error?
raise "Couldn't find thumbprint for subject #{new_resource.cert_name}" if p.stdout.nil? || p.stdout.empty?
# seem to get a UTF-8 string with BOM returned sometimes! Strip any such BOM
hash = p.stdout.strip
hash[0].ord == 239 ? hash.force_encoding('UTF-8').delete!("\xEF\xBB\xBF".force_encoding('UTF-8')) : hash
end
end