leap_cli 1.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. data/bin/leap +81 -0
  2. data/lib/core_ext/boolean.rb +14 -0
  3. data/lib/core_ext/hash.rb +35 -0
  4. data/lib/core_ext/json.rb +42 -0
  5. data/lib/core_ext/nil.rb +5 -0
  6. data/lib/core_ext/string.rb +14 -0
  7. data/lib/leap/platform.rb +52 -0
  8. data/lib/leap_cli/commands/ca.rb +430 -0
  9. data/lib/leap_cli/commands/clean.rb +16 -0
  10. data/lib/leap_cli/commands/compile.rb +134 -0
  11. data/lib/leap_cli/commands/deploy.rb +172 -0
  12. data/lib/leap_cli/commands/facts.rb +93 -0
  13. data/lib/leap_cli/commands/inspect.rb +140 -0
  14. data/lib/leap_cli/commands/list.rb +122 -0
  15. data/lib/leap_cli/commands/new.rb +126 -0
  16. data/lib/leap_cli/commands/node.rb +272 -0
  17. data/lib/leap_cli/commands/pre.rb +99 -0
  18. data/lib/leap_cli/commands/shell.rb +67 -0
  19. data/lib/leap_cli/commands/test.rb +55 -0
  20. data/lib/leap_cli/commands/user.rb +140 -0
  21. data/lib/leap_cli/commands/util.rb +50 -0
  22. data/lib/leap_cli/commands/vagrant.rb +201 -0
  23. data/lib/leap_cli/config/macros.rb +369 -0
  24. data/lib/leap_cli/config/manager.rb +369 -0
  25. data/lib/leap_cli/config/node.rb +37 -0
  26. data/lib/leap_cli/config/object.rb +336 -0
  27. data/lib/leap_cli/config/object_list.rb +174 -0
  28. data/lib/leap_cli/config/secrets.rb +43 -0
  29. data/lib/leap_cli/config/tag.rb +18 -0
  30. data/lib/leap_cli/constants.rb +7 -0
  31. data/lib/leap_cli/leapfile.rb +97 -0
  32. data/lib/leap_cli/load_paths.rb +15 -0
  33. data/lib/leap_cli/log.rb +166 -0
  34. data/lib/leap_cli/logger.rb +216 -0
  35. data/lib/leap_cli/markdown_document_listener.rb +134 -0
  36. data/lib/leap_cli/path.rb +84 -0
  37. data/lib/leap_cli/remote/leap_plugin.rb +204 -0
  38. data/lib/leap_cli/remote/puppet_plugin.rb +66 -0
  39. data/lib/leap_cli/remote/rsync_plugin.rb +35 -0
  40. data/lib/leap_cli/remote/tasks.rb +36 -0
  41. data/lib/leap_cli/requirements.rb +19 -0
  42. data/lib/leap_cli/ssh_key.rb +130 -0
  43. data/lib/leap_cli/util/remote_command.rb +110 -0
  44. data/lib/leap_cli/util/secret.rb +54 -0
  45. data/lib/leap_cli/util/x509.rb +32 -0
  46. data/lib/leap_cli/util.rb +431 -0
  47. data/lib/leap_cli/version.rb +9 -0
  48. data/lib/leap_cli.rb +46 -0
  49. data/lib/lib_ext/capistrano_connections.rb +16 -0
  50. data/lib/lib_ext/gli.rb +52 -0
  51. data/lib/lib_ext/markdown_document_listener.rb +122 -0
  52. data/vendor/certificate_authority/lib/certificate_authority/certificate.rb +200 -0
  53. data/vendor/certificate_authority/lib/certificate_authority/certificate_revocation_list.rb +77 -0
  54. data/vendor/certificate_authority/lib/certificate_authority/distinguished_name.rb +97 -0
  55. data/vendor/certificate_authority/lib/certificate_authority/extensions.rb +266 -0
  56. data/vendor/certificate_authority/lib/certificate_authority/key_material.rb +148 -0
  57. data/vendor/certificate_authority/lib/certificate_authority/ocsp_handler.rb +144 -0
  58. data/vendor/certificate_authority/lib/certificate_authority/pkcs11_key_material.rb +65 -0
  59. data/vendor/certificate_authority/lib/certificate_authority/revocable.rb +14 -0
  60. data/vendor/certificate_authority/lib/certificate_authority/serial_number.rb +10 -0
  61. data/vendor/certificate_authority/lib/certificate_authority/signing_entity.rb +16 -0
  62. data/vendor/certificate_authority/lib/certificate_authority/signing_request.rb +56 -0
  63. data/vendor/certificate_authority/lib/certificate_authority.rb +21 -0
  64. data/vendor/rsync_command/lib/rsync_command/ssh_options.rb +159 -0
  65. data/vendor/rsync_command/lib/rsync_command/thread_pool.rb +36 -0
  66. data/vendor/rsync_command/lib/rsync_command/version.rb +3 -0
  67. data/vendor/rsync_command/lib/rsync_command.rb +96 -0
  68. data/vendor/rsync_command/test/rsync_test.rb +74 -0
  69. data/vendor/rsync_command/test/ssh_options_test.rb +61 -0
  70. data/vendor/vagrant_ssh_keys/vagrant.key +27 -0
  71. data/vendor/vagrant_ssh_keys/vagrant.pub +1 -0
  72. metadata +345 -0
data/bin/leap ADDED
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env ruby
2
+ begin
3
+ require 'leap_cli'
4
+ rescue LoadError
5
+ #
6
+ # When developing a gem with a command, you normally use `bundle exec bin/command-name`
7
+ # to run your app. At install-time, RubyGems will make sure lib, etc. are in the load path,
8
+ # so that you can run the command directly.
9
+ #
10
+ # However, I don't like using 'bundle exec'. It is slow, and limits which directory you can
11
+ # run in. So, instead, we fall back to some path manipulation hackery.
12
+ #
13
+ # This allows you to run the command directly while developing the gem, and also lets you
14
+ # run from anywhere (I like to link 'bin/leap' to /usr/local/bin/leap).
15
+ #
16
+ require 'rubygems'
17
+ base_dir = File.expand_path('..', File.dirname(File.symlink?(__FILE__) ? File.readlink(__FILE__) : __FILE__))
18
+ require File.join(base_dir, 'lib','leap_cli','load_paths')
19
+ require 'leap_cli'
20
+ end
21
+
22
+ require 'gli'
23
+ require 'highline'
24
+ require 'forwardable'
25
+ require 'lib_ext/gli' # our custom extensions to gli
26
+
27
+ #
28
+ # Typically, GLI and Highline methods are loaded into the global namespace.
29
+ # Instead, here we load these into the module LeapCli::Commands in order to
30
+ # ensure that the cli logic and code is kept isolated to leap_cli/commands/*.rb
31
+ #
32
+ # no cheating!
33
+ #
34
+ module LeapCli::Commands
35
+ extend GLI::App
36
+ extend Forwardable
37
+
38
+ # delegate highline methods to make them available to sub-commands
39
+ @terminal = HighLine.new
40
+ def_delegator :@terminal, :ask, 'self.ask'
41
+ def_delegator :@terminal, :agree, 'self.agree'
42
+ def_delegator :@terminal, :choose, 'self.choose'
43
+ def_delegator :@terminal, :say, 'self.say'
44
+ def_delegator :@terminal, :color, 'self.color'
45
+ def_delegator :@terminal, :list, 'self.list'
46
+
47
+ # make config manager available as 'manager'
48
+ def self.manager
49
+ @manager ||= begin
50
+ manager = LeapCli::Config::Manager.new
51
+ manager.load
52
+ manager
53
+ end
54
+ end
55
+
56
+ # make provider config available as 'provider'
57
+ def self.provider
58
+ manager.provider
59
+ end
60
+
61
+ # make leapfile available as 'leapfile'
62
+ def self.leapfile
63
+ LeapCli::leapfile
64
+ end
65
+
66
+ # info about leap command line suite
67
+ program_desc LeapCli::SUMMARY
68
+ program_long_desc LeapCli::DESCRIPTION
69
+
70
+ # handle --version ourselves
71
+ if ARGV.grep(/--version/).any?
72
+ puts "leap #{LeapCli::VERSION}, ruby #{RUBY_VERSION}"
73
+ exit(0)
74
+ end
75
+
76
+ # load commands and run
77
+ commands_from('leap_cli/commands')
78
+ ORIGINAL_ARGV = ARGV.dup
79
+ exit_status = run(ARGV)
80
+ exit(LeapCli::Util.exit_status || exit_status)
81
+ end
@@ -0,0 +1,14 @@
1
+ #
2
+ # make is_a?(Boolean) possible.
3
+ #
4
+
5
+ module Boolean
6
+ end
7
+
8
+ class TrueClass
9
+ include Boolean
10
+ end
11
+
12
+ class FalseClass
13
+ include Boolean
14
+ end
@@ -0,0 +1,35 @@
1
+ class Hash
2
+
3
+ ##
4
+ ## CONVERTING
5
+ ##
6
+
7
+ #
8
+ # convert self into a hash, but only include the specified keys
9
+ #
10
+ def pick(*keys)
11
+ keys.map(&:to_s).inject({}) do |hsh, key|
12
+ if has_key?(key)
13
+ hsh[key] = self[key]
14
+ end
15
+ hsh
16
+ end
17
+ end
18
+
19
+ #
20
+ # recursive merging (aka deep merge)
21
+ # taken from ActiveSupport::CoreExtensions::Hash::DeepMerge
22
+ #
23
+ def deep_merge(other_hash)
24
+ self.merge(other_hash) do |key, oldval, newval|
25
+ oldval = oldval.to_hash if oldval.respond_to?(:to_hash)
26
+ newval = newval.to_hash if newval.respond_to?(:to_hash)
27
+ oldval.class.to_s == 'Hash' && newval.class.to_s == 'Hash' ? oldval.deep_merge(newval) : newval
28
+ end
29
+ end
30
+
31
+ def deep_merge!(other_hash)
32
+ replace(deep_merge(other_hash))
33
+ end
34
+
35
+ end
@@ -0,0 +1,42 @@
1
+ module JSON
2
+ #
3
+ # Output JSON from ruby objects in such a manner that all the hashes and arrays are output in alphanumeric sorted order.
4
+ # This is required so that our generated configs don't throw puppet or git for a tizzy fit.
5
+ #
6
+ # Beware: some hacky stuff ahead.
7
+ #
8
+ # This relies on the pure ruby implementation of JSON.generate (i.e. require 'json/pure')
9
+ # see https://github.com/flori/json/blob/master/lib/json/pure/generator.rb
10
+ #
11
+ # The Oj way that we are not using: Oj.dump(obj, :mode => :compat, :indent => 2)
12
+ #
13
+ def self.sorted_generate(obj)
14
+ # modify hash and array
15
+ Array.class_eval do
16
+ alias_method :each_without_sort, :each
17
+ def each(&block)
18
+ sorted = sort {|a,b| a.to_s <=> b.to_s }
19
+ for i in 0..(sorted.length-1) do
20
+ yield sorted[i]
21
+ end
22
+ end
23
+ end
24
+ Hash.class_eval do
25
+ alias_method :each_without_sort, :each
26
+ def each(&block)
27
+ self.keys.each do |key|
28
+ yield key, self.fetch(key) # fetch is used so we don't trigger Config::Object auto-eval
29
+ end
30
+ end
31
+ end
32
+
33
+ # generate json
34
+ json_str = JSON.pretty_generate(obj)
35
+
36
+ # restore hash and array
37
+ Hash.class_eval {alias_method :each, :each_without_sort}
38
+ Array.class_eval {alias_method :each, :each_without_sort}
39
+
40
+ return json_str
41
+ end
42
+ end
@@ -0,0 +1,5 @@
1
+ class NilClass
2
+ def any?
3
+ false
4
+ end
5
+ end
@@ -0,0 +1,14 @@
1
+ #
2
+ # make ruby 1.9 act more like ruby 1.8
3
+ #
4
+ unless String.method_defined?(:to_a)
5
+ class String
6
+ def to_a; [self]; end
7
+ end
8
+ end
9
+
10
+ unless String.method_defined?(:any?)
11
+ class String
12
+ def any?; self.chars.any?; end
13
+ end
14
+ end
@@ -0,0 +1,52 @@
1
+ require 'versionomy'
2
+
3
+ module Leap
4
+
5
+ class Platform
6
+ class << self
7
+ #
8
+ # configuration
9
+ #
10
+
11
+ attr_reader :version
12
+ attr_reader :compatible_cli
13
+ attr_accessor :facts
14
+ attr_accessor :paths
15
+ attr_accessor :node_files
16
+ attr_accessor :puppet_destination
17
+
18
+ def define(&block)
19
+ self.instance_eval(&block)
20
+ end
21
+
22
+ def version=(version)
23
+ @version = Versionomy.parse(version)
24
+ end
25
+
26
+ def compatible_cli=(range)
27
+ @compatible_cli = range
28
+ @minimum_cli_version = Versionomy.parse(range.first)
29
+ @maximum_cli_version = Versionomy.parse(range.last)
30
+ end
31
+
32
+ #
33
+ # return true if the cli_version is compatible with this platform.
34
+ #
35
+ def compatible_with_cli?(cli_version)
36
+ cli_version = Versionomy.parse(cli_version)
37
+ cli_version >= @minimum_cli_version && cli_version <= @maximum_cli_version
38
+ end
39
+
40
+ #
41
+ # return true if the platform version is within the specified range.
42
+ #
43
+ def version_in_range?(range)
44
+ minimum_platform_version = Versionomy.parse(range.first)
45
+ maximum_platform_version = Versionomy.parse(range.last)
46
+ @version >= minimum_platform_version && @version <= maximum_platform_version
47
+ end
48
+ end
49
+
50
+ end
51
+
52
+ end
@@ -0,0 +1,430 @@
1
+ require 'openssl'
2
+ require 'certificate_authority'
3
+ require 'date'
4
+ require 'digest/md5'
5
+
6
+ module LeapCli; module Commands
7
+
8
+ desc "Manage X.509 certificates"
9
+ command :cert do |cert|
10
+
11
+ cert.desc 'Creates two Certificate Authorities (one for validating servers and one for validating clients).'
12
+ cert.long_desc 'See see what values are used in the generation of the certificates (like name and key size), run `leap inspect provider` and look for the "ca" property. To see the details of the created certs, run `leap inspect <file>`.'
13
+ cert.command :ca do |ca|
14
+ ca.action do |global_options,options,args|
15
+ assert_config! 'provider.ca.name'
16
+ generate_new_certificate_authority(:ca_key, :ca_cert, provider.ca.name)
17
+ generate_new_certificate_authority(:client_ca_key, :client_ca_cert, provider.ca.name + ' (client certificates only!)')
18
+ end
19
+ end
20
+
21
+ cert.desc 'Creates or renews a X.509 certificate/key pair for a single node or all nodes, but only if needed.'
22
+ cert.long_desc 'This command will a generate new certificate for a node if some value in the node has changed ' +
23
+ 'that is included in the certificate (like hostname or IP address), or if the old certificate will be expiring soon. ' +
24
+ 'Sometimes, you might want to force the generation of a new certificate, ' +
25
+ 'such as in the cases where you have changed a CA parameter for server certificates, like bit size or digest hash. ' +
26
+ 'In this case, use --force. If <node-filter> is empty, this command will apply to all nodes.'
27
+ cert.arg_name 'FILTER'
28
+ cert.command :update do |update|
29
+ update.switch 'force', :desc => 'Always generate new certificates', :negatable => false
30
+ update.action do |global_options,options,args|
31
+ assert_files_exist! :ca_cert, :ca_key, :msg => 'Run `leap cert ca` to create them'
32
+ assert_config! 'provider.ca.server_certificates.bit_size'
33
+ assert_config! 'provider.ca.server_certificates.digest'
34
+ assert_config! 'provider.ca.server_certificates.life_span'
35
+ assert_config! 'common.x509.use'
36
+
37
+ nodes = manager.filter!(args)
38
+ nodes.each_node do |node|
39
+ if !node.x509.use
40
+ remove_file!([:node_x509_key, node.name])
41
+ remove_file!([:node_x509_cert, node.name])
42
+ elsif options[:force] || cert_needs_updating?(node)
43
+ generate_cert_for_node(node)
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ cert.desc 'Creates a Diffie-Hellman parameter file.' # (needed for server-side of some TLS connections)
50
+ cert.command :dh do |dh|
51
+ dh.action do |global_options,options,args|
52
+ long_running do
53
+ if cmd_exists?('certtool')
54
+ log 0, 'Generating DH parameters (takes a long time)...'
55
+ output = assert_run!('certtool --generate-dh-params --sec-param high')
56
+ output.sub! /.*(-----BEGIN DH PARAMETERS-----.*-----END DH PARAMETERS-----).*/m, '\1'
57
+ output << "\n"
58
+ write_file!(:dh_params, output)
59
+ else
60
+ log 0, 'Generating DH parameters (takes a REALLY long time)...'
61
+ output = OpenSSL::PKey::DH.generate(3248).to_pem
62
+ write_file!(:dh_params, output)
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ #
69
+ # hints:
70
+ #
71
+ # inspect CSR:
72
+ # openssl req -noout -text -in files/cert/x.csr
73
+ #
74
+ # generate CSR with openssl to see how it compares:
75
+ # openssl req -sha256 -nodes -newkey rsa:2048 -keyout example.key -out example.csr
76
+ #
77
+ # validate a CSR:
78
+ # http://certlogik.com/decoder/
79
+ #
80
+ # nice details about CSRs:
81
+ # http://www.redkestrel.co.uk/Articles/CSR.html
82
+ #
83
+ cert.desc "Creates a CSR for use in buying a commercial X.509 certificate."
84
+ cert.long_desc "Unless specified, the CSR is created for the provider's primary domain. The properties used for this CSR come from `provider.ca.server_certificates`."
85
+ cert.command :csr do |csr|
86
+ csr.flag 'domain', :arg_name => 'DOMAIN', :desc => 'Specify what domain to create the CSR for.'
87
+ csr.action do |global_options,options,args|
88
+ assert_config! 'provider.domain'
89
+ assert_config! 'provider.name'
90
+ assert_config! 'provider.default_language'
91
+ assert_config! 'provider.ca.server_certificates.bit_size'
92
+ assert_config! 'provider.ca.server_certificates.digest'
93
+ domain = options[:domain] || provider.domain
94
+ assert_files_missing! [:commercial_key, domain], [:commercial_csr, domain], :msg => 'If you really want to create a new key and CSR, remove these files first.'
95
+
96
+ server_certificates = provider.ca.server_certificates
97
+
98
+ # RSA key
99
+ keypair = CertificateAuthority::MemoryKeyMaterial.new
100
+ log :generating, "%s bit RSA key" % server_certificates.bit_size do
101
+ keypair.generate_key(server_certificates.bit_size)
102
+ write_file! [:commercial_key, domain], keypair.private_key.to_pem
103
+ end
104
+
105
+ # CSR
106
+ dn = CertificateAuthority::DistinguishedName.new
107
+ csr = CertificateAuthority::SigningRequest.new
108
+ dn.common_name = domain
109
+ dn.organization = provider.name[provider.default_language]
110
+ dn.country = server_certificates['country'] # optional
111
+ dn.state = server_certificates['state'] # optional
112
+ dn.locality = server_certificates['locality'] # optional
113
+
114
+ log :generating, "CSR with commonName => '%s', organization => '%s'" % [dn.common_name, dn.organization] do
115
+ csr.distinguished_name = dn
116
+ csr.key_material = keypair
117
+ csr.digest = server_certificates.digest
118
+ request = csr.to_x509_csr
119
+ write_file! [:commercial_csr, domain], csr.to_pem
120
+ end
121
+
122
+ # Sign using our own CA, for use in testing but hopefully not production.
123
+ # It is not that commerical CAs are so secure, it is just that signing your own certs is
124
+ # a total drag for the user because they must click through dire warnings.
125
+ #if options[:sign]
126
+ log :generating, "self-signed x509 server certificate for testing purposes" do
127
+ cert = csr.to_cert
128
+ cert.serial_number.number = cert_serial_number(domain)
129
+ cert.not_before = yesterday
130
+ cert.not_after = years_from_yesterday(1)
131
+ cert.parent = ca_root
132
+ cert.sign! domain_test_signing_profile
133
+ write_file! [:commercial_cert, domain], cert.to_pem
134
+ log "please replace this file with the real certificate you get from a CA using #{Path.relative_path([:commercial_csr, domain])}"
135
+ end
136
+ #end
137
+
138
+ # FAKE CA
139
+ unless file_exists? :commercial_ca_cert
140
+ log :using, "generated CA in place of commercial CA for testing purposes" do
141
+ write_file! :commercial_ca_cert, read_file!(:ca_cert)
142
+ log "please also replace this file with the CA cert from the commercial authority you use."
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
148
+
149
+ private
150
+
151
+ def generate_new_certificate_authority(key_file, cert_file, common_name)
152
+ assert_files_missing! key_file, cert_file
153
+ assert_config! 'provider.ca.name'
154
+ assert_config! 'provider.ca.bit_size'
155
+ assert_config! 'provider.ca.life_span'
156
+
157
+ root = CertificateAuthority::Certificate.new
158
+
159
+ # set subject
160
+ root.subject.common_name = common_name
161
+ possible = ['country', 'state', 'locality', 'organization', 'organizational_unit', 'email_address']
162
+ provider.ca.keys.each do |key|
163
+ if possible.include?(key)
164
+ root.subject.send(key + '=', provider.ca[key])
165
+ end
166
+ end
167
+
168
+ # set expiration
169
+ root.not_before = yesterday
170
+ root.not_after = years_from_yesterday(provider.ca.life_span.to_i)
171
+
172
+ # generate private key
173
+ root.serial_number.number = 1
174
+ root.key_material.generate_key(provider.ca.bit_size)
175
+
176
+ # sign self
177
+ root.signing_entity = true
178
+ root.parent = root
179
+ root.sign!(ca_root_signing_profile)
180
+
181
+ # save
182
+ write_file!(key_file, root.key_material.private_key.to_pem)
183
+ write_file!(cert_file, root.to_pem)
184
+ end
185
+
186
+ #
187
+ # returns true if the certs associated with +node+ need to be regenerated.
188
+ #
189
+ def cert_needs_updating?(node)
190
+ if !file_exists?([:node_x509_cert, node.name], [:node_x509_key, node.name])
191
+ return true
192
+ else
193
+ cert = load_certificate_file([:node_x509_cert, node.name])
194
+ if cert.not_after < months_from_yesterday(1)
195
+ log :updating, "cert for node '#{node.name}' because it will expire soon"
196
+ return true
197
+ end
198
+ if cert.subject.common_name != node.domain.full
199
+ log :updating, "cert for node '#{node.name}' because domain.full has changed"
200
+ return true
201
+ end
202
+ cert.openssl_body.extensions.each do |ext|
203
+ if ext.oid == "subjectAltName"
204
+ ips = []
205
+ dns_names = []
206
+ ext.value.split(",").each do |value|
207
+ value.strip!
208
+ ips << $1 if value =~ /^IP Address:(.*)$/
209
+ dns_names << $1 if value =~ /^DNS:(.*)$/
210
+ end
211
+ if ips.first != node.ip_address
212
+ log :updating, "cert for node '#{node.name}' because ip_address has changed (from #{ips.first} to #{node.ip_address})"
213
+ return true
214
+ elsif dns_names != dns_names_for_node(node)
215
+ log :updating, "cert for node '#{node.name}' because domain name aliases have changed (from #{dns_names.inspect} to #{dns_names_for_node(node).inspect})"
216
+ return true
217
+ end
218
+ end
219
+ end
220
+ end
221
+ return false
222
+ end
223
+
224
+ def generate_cert_for_node(node)
225
+ return if node.x509.use == false
226
+
227
+ cert = CertificateAuthority::Certificate.new
228
+
229
+ # set subject
230
+ cert.subject.common_name = node.domain.full
231
+ cert.serial_number.number = cert_serial_number(node.domain.full)
232
+
233
+ # set expiration
234
+ cert.not_before = yesterday
235
+ cert.not_after = years_from_yesterday(provider.ca.server_certificates.life_span.to_i)
236
+
237
+ # generate key
238
+ cert.key_material.generate_key(provider.ca.server_certificates.bit_size)
239
+
240
+ # sign
241
+ cert.parent = ca_root
242
+ cert.sign!(server_signing_profile(node))
243
+
244
+ # save
245
+ write_file!([:node_x509_key, node.name], cert.key_material.private_key.to_pem)
246
+ write_file!([:node_x509_cert, node.name], cert.to_pem)
247
+ end
248
+
249
+ #
250
+ # yields client key and cert suitable for testing
251
+ #
252
+ def generate_test_client_cert(prefix=nil)
253
+ cert = CertificateAuthority::Certificate.new
254
+ cert.serial_number.number = cert_serial_number(provider.domain)
255
+ cert.subject.common_name = [prefix, random_common_name(provider.domain)].join
256
+ cert.not_before = yesterday
257
+ cert.not_after = years_from_yesterday(1)
258
+ cert.key_material.generate_key(1024) # just for testing, remember!
259
+ cert.parent = client_ca_root
260
+ cert.sign! client_test_signing_profile
261
+ yield cert.key_material.private_key.to_pem, cert.to_pem
262
+ end
263
+
264
+ def ca_root
265
+ @ca_root ||= begin
266
+ load_certificate_file(:ca_cert, :ca_key)
267
+ end
268
+ end
269
+
270
+ def client_ca_root
271
+ @client_ca_root ||= begin
272
+ load_certificate_file(:client_ca_cert, :client_ca_key)
273
+ end
274
+ end
275
+
276
+ def load_certificate_file(crt_file, key_file=nil, password=nil)
277
+ crt = read_file!(crt_file)
278
+ openssl_cert = OpenSSL::X509::Certificate.new(crt)
279
+ cert = CertificateAuthority::Certificate.from_openssl(openssl_cert)
280
+ if key_file
281
+ key = read_file!(key_file)
282
+ cert.key_material.private_key = OpenSSL::PKey::RSA.new(key, password)
283
+ end
284
+ return cert
285
+ end
286
+
287
+ def ca_root_signing_profile
288
+ {
289
+ "extensions" => {
290
+ "basicConstraints" => {"ca" => true},
291
+ "keyUsage" => {
292
+ "usage" => ["critical", "keyCertSign"]
293
+ },
294
+ "extendedKeyUsage" => {
295
+ "usage" => []
296
+ }
297
+ }
298
+ }
299
+ end
300
+
301
+ #
302
+ # For keyusage, openvpn server certs can have keyEncipherment or keyAgreement.
303
+ # Web browsers seem to break without keyEncipherment.
304
+ # For now, I am using digitalSignature + keyEncipherment
305
+ #
306
+ # * digitalSignature -- for (EC)DHE cipher suites
307
+ # "The digitalSignature bit is asserted when the subject public key is used
308
+ # with a digital signature mechanism to support security services other
309
+ # than certificate signing (bit 5), or CRL signing (bit 6). Digital
310
+ # signature mechanisms are often used for entity authentication and data
311
+ # origin authentication with integrity."
312
+ #
313
+ # * keyEncipherment ==> for plain RSA cipher suites
314
+ # "The keyEncipherment bit is asserted when the subject public key is used for
315
+ # key transport. For example, when an RSA key is to be used for key management,
316
+ # then this bit is set."
317
+ #
318
+ # * keyAgreement ==> for used with DH, not RSA.
319
+ # "The keyAgreement bit is asserted when the subject public key is used for key
320
+ # agreement. For example, when a Diffie-Hellman key is to be used for key
321
+ # management, then this bit is set."
322
+ #
323
+ # digest options: SHA512, SHA256, SHA1
324
+ #
325
+ def server_signing_profile(node)
326
+ {
327
+ "digest" => provider.ca.server_certificates.digest,
328
+ "extensions" => {
329
+ "keyUsage" => {
330
+ "usage" => ["digitalSignature", "keyEncipherment"]
331
+ },
332
+ "extendedKeyUsage" => {
333
+ "usage" => ["serverAuth", "clientAuth"]
334
+ },
335
+ "subjectAltName" => {
336
+ "ips" => [node.ip_address],
337
+ "dns_names" => dns_names_for_node(node)
338
+ }
339
+ }
340
+ }
341
+ end
342
+
343
+ #
344
+ # This is used when signing the main cert for the provider's domain
345
+ # with our own CA (for testing purposes). Typically, this cert would
346
+ # be purchased from a commercial CA, and not signed this way.
347
+ #
348
+ def domain_test_signing_profile
349
+ {
350
+ "digest" => "SHA256",
351
+ "extensions" => {
352
+ "keyUsage" => {
353
+ "usage" => ["digitalSignature", "keyEncipherment"]
354
+ },
355
+ "extendedKeyUsage" => {
356
+ "usage" => ["serverAuth"]
357
+ }
358
+ }
359
+ }
360
+ end
361
+
362
+ #
363
+ # This is used when signing a dummy client certificate that is only to be
364
+ # used for testing.
365
+ #
366
+ def client_test_signing_profile
367
+ {
368
+ "digest" => "SHA256",
369
+ "extensions" => {
370
+ "keyUsage" => {
371
+ "usage" => ["digitalSignature"]
372
+ },
373
+ "extendedKeyUsage" => {
374
+ "usage" => ["clientAuth"]
375
+ }
376
+ }
377
+ }
378
+ end
379
+
380
+ def dns_names_for_node(node)
381
+ names = [node.domain.internal, node.domain.full]
382
+ if node['dns'] && node.dns['aliases'] && node.dns.aliases.any?
383
+ names += node.dns.aliases
384
+ names.compact!
385
+ end
386
+ return names
387
+ end
388
+
389
+ #
390
+ # For cert serial numbers, we need a non-colliding number less than 160 bits.
391
+ # md5 will do nicely, since there is no need for a secure hash, just a short one.
392
+ # (md5 is 128 bits)
393
+ #
394
+ def cert_serial_number(domain_name)
395
+ Digest::MD5.hexdigest("#{domain_name} -- #{Time.now}").to_i(16)
396
+ end
397
+
398
+ #
399
+ # for the random common name, we need a text string that will be unique across all certs.
400
+ # ruby 1.8 doesn't have a built-in uuid generator, or we would use SecureRandom.uuid
401
+ #
402
+ def random_common_name(domain_name)
403
+ cert_serial_number(domain_name).to_s(36)
404
+ end
405
+
406
+ ##
407
+ ## TIME HELPERS
408
+ ##
409
+ ## note: we use 'yesterday' instead of 'today', because times are in UTC, and some people on the planet
410
+ ## are behind UTC.
411
+ ##
412
+
413
+ def yesterday
414
+ t = Time.now - 24*24*60
415
+ Time.utc t.year, t.month, t.day
416
+ end
417
+
418
+ def years_from_yesterday(num)
419
+ t = yesterday
420
+ Time.utc t.year + num, t.month, t.day
421
+ end
422
+
423
+ def months_from_yesterday(num)
424
+ t = yesterday
425
+ date = Date.new t.year, t.month, t.day
426
+ date = date >> num # >> is months in the future operator
427
+ Time.utc date.year, date.month, date.day
428
+ end
429
+
430
+ end; end
@@ -0,0 +1,16 @@
1
+ module LeapCli
2
+ module Commands
3
+
4
+ desc 'Removes all files generated with the "compile" command.'
5
+ command :clean do |c|
6
+ c.action do |global_options,options,args|
7
+ Dir.glob(path([:hiera, '*'])).each do |file|
8
+ remove_file! file
9
+ end
10
+ remove_file! path(:authorized_keys)
11
+ remove_file! path(:known_hosts)
12
+ end
13
+ end
14
+
15
+ end
16
+ end