leap_cli 1.2.5

Sign up to get free protection for your applications and to get access to all the features.
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