aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorelijah <elijah@riseup.net>2012-11-01 01:07:27 -0700
committerelijah <elijah@riseup.net>2012-11-01 01:07:27 -0700
commit57287ae1d4151ec453ec9d33fafe4f1a4ced37e0 (patch)
treefe63a8de11c41d247fc3634277bc49c1ca7cd689
parentf339e7b94ab8920fd1e271c50145b5d2d1a8ac9d (diff)
downloadleap_cli-57287ae1d4151ec453ec9d33fafe4f1a4ced37e0.tar.gz
leap_cli-57287ae1d4151ec453ec9d33fafe4f1a4ced37e0.tar.bz2
x.509 support -- added certificate authority creation and server cert creation
-rwxr-xr-xbin/leap5
-rw-r--r--leap_cli.gemspec8
-rw-r--r--lib/leap_cli/commands/ca.rb162
-rw-r--r--lib/leap_cli/commands/node.rb2
-rw-r--r--lib/leap_cli/commands/project.rb2
-rw-r--r--lib/leap_cli/config/manager.rb23
-rw-r--r--lib/leap_cli/config/object.rb6
-rw-r--r--lib/leap_cli/path.rb14
-rw-r--r--lib/leap_cli/util.rb74
-rw-r--r--lib/leap_cli/version.rb2
-rw-r--r--test/provider/common.json5
-rw-r--r--test/provider/provider.json9
-rw-r--r--test/provider/services/openvpn.json5
13 files changed, 280 insertions, 37 deletions
diff --git a/bin/leap b/bin/leap
index dd6d9de..582d313 100755
--- a/bin/leap
+++ b/bin/leap
@@ -46,6 +46,7 @@ module LeapCli::Commands
def_delegator :@terminal, :agree, 'self.agree'
def_delegator :@terminal, :choose, 'self.choose'
def_delegator :@terminal, :say, 'self.say'
+ def_delegator :@terminal, :color, 'self.color'
#
# make config manager available as 'manager'
@@ -61,8 +62,8 @@ module LeapCli::Commands
#
# info about leap command line suite
#
- program_desc 'LEAP platform command line interface'
- program_long_desc 'This is the long description. It is very interesting.'
+ program_desc LeapCli::SUMMARY
+ program_long_desc LeapCli::DESCRIPTION
version LeapCli::VERSION
#
diff --git a/leap_cli.gemspec b/leap_cli.gemspec
index ecabe45..20e50a8 100644
--- a/leap_cli.gemspec
+++ b/leap_cli.gemspec
@@ -16,6 +16,7 @@ spec = Gem::Specification.new do |s|
s.platform = Gem::Platform::RUBY
s.summary = LeapCli::SUMMARY
s.description = LeapCli::DESCRIPTION
+ s.license = "GPLv3"
##
## GEM FILES
@@ -48,13 +49,16 @@ spec = Gem::Specification.new do |s|
s.add_runtime_dependency('highline')
# network gems
- s.add_runtime_dependency('net-ssh')
s.add_runtime_dependency('capistrano')
#s.add_runtime_dependency('supply_drop')
+ # crypto gems
+ s.add_runtime_dependency('certificate_authority') # this gem pulls in ActiveModel, but it just uses it for validation logic.
+ s.add_runtime_dependency('net-ssh')
+ s.add_runtime_dependency('gpgme') # not essential, but used for some minor stuff in adding sysadmins
+
# misc gems
s.add_runtime_dependency('ya2yaml') # pure ruby yaml, so we can better control output. see https://github.com/afunai/ya2yaml
s.add_runtime_dependency('json_pure') # pure ruby json, so we can better control output.
- s.add_runtime_dependency('gpgme') # not essential, but used for some minor stuff in adding sysadmins
end
diff --git a/lib/leap_cli/commands/ca.rb b/lib/leap_cli/commands/ca.rb
new file mode 100644
index 0000000..9f1d42e
--- /dev/null
+++ b/lib/leap_cli/commands/ca.rb
@@ -0,0 +1,162 @@
+require 'openssl'
+require 'certificate_authority'
+require 'date'
+require 'digest/md5'
+
+module LeapCli; module Commands
+
+ desc 'Creates the public and private key for your Certificate Authority.'
+ command :'init-ca' do |c|
+ c.action do |global_options,options,args|
+ assert_files_missing! :ca_cert, :ca_key
+ assert_config! 'provider.ca.name'
+ assert_config! 'provider.ca.bit_size'
+
+ provider = manager.provider
+ root = CertificateAuthority::Certificate.new
+
+ # set subject
+ root.subject.common_name = provider.ca.name
+ possible = ['country', 'state', 'locality', 'organization', 'organizational_unit', 'email_address']
+ provider.ca.keys.each do |key|
+ if possible.include?(key)
+ root.subject.send(key + '=', provider.ca[key])
+ end
+ end
+
+ # set expiration
+ years = 2
+ today = Date.today
+ root.not_before = Time.gm today.year, today.month, today.day
+ root.not_after = root.not_before + years * 60 * 60 * 24 * 365
+
+ # generate private key
+ root.serial_number.number = 1
+ root.key_material.generate_key(provider.ca.bit_size)
+
+ # sign self
+ root.signing_entity = true
+ root.parent = root
+ root.sign!(ca_root_signing_profile)
+
+ # save
+ write_file!(:ca_key, root.key_material.private_key.to_pem)
+ write_file!(:ca_cert, root.to_pem)
+ end
+ end
+
+ desc 'Creates or renews a X.509 certificate/key pair for a single node or all nodes'
+ arg_name '<node-name | "all">', :optional => false, :multiple => false
+ command :'update-cert' do |c|
+ c.action do |global_options,options,args|
+ assert_files_exist! :ca_cert, :ca_key, :msg => 'Run init-ca to create them'
+ assert_config! 'provider.ca.server_certificates.bit_size'
+ assert_config! 'provider.ca.server_certificates.life_span'
+
+ if args.first == 'all'
+ bail! 'not supported yet'
+ else
+ provider = manager.provider
+ ca_root = cert_from_files(:ca_cert, :ca_key)
+ node = get_node_from_args(args)
+
+ # set subject
+ cert = CertificateAuthority::Certificate.new
+ cert.subject.common_name = node.domain.full
+
+ # set expiration
+ years = provider.ca.server_certificates.life_span.to_i
+ today = Date.today
+ cert.not_before = Time.gm today.year, today.month, today.day
+ cert.not_after = cert.not_before + years * 60 * 60 * 24 * 365
+
+ # generate key
+ cert.serial_number.number = cert_serial_number(node.domain.full)
+ cert.key_material.generate_key(provider.ca.server_certificates.bit_size)
+
+ # sign
+ cert.parent = ca_root
+ cert.sign!(server_signing_profile(node))
+
+ # save
+ write_file!([:node_x509_key, node.name], cert.key_material.private_key.to_pem)
+ write_file!([:node_x509_cert, node.name], cert.to_pem)
+ end
+ end
+ end
+
+ desc 'Generates Diffie-Hellman parameter file (needed for server-side of TLS connections)'
+ command :'init-dh' do |c|
+ c.action do |global_options,options,args|
+ long_running do
+ if cmd_exists?('certtool')
+ progress('Generating DH parameters (takes a long time)...')
+ output = assert_run!('certtool --generate-dh-params --sec-param high')
+ write_file!(:dh_params, output)
+ else
+ progress('Generating DH parameters (takes a REALLY long time)...')
+ output = OpenSSL::PKey::DH.generate(3248).to_pem
+ write_file!(:dh_params, output)
+ end
+ end
+ end
+ end
+
+ private
+
+ def cert_from_files(crt, key)
+ crt = read_file!(crt)
+ key = read_file!(key)
+ openssl_cert = OpenSSL::X509::Certificate.new(crt)
+ cert = CertificateAuthority::Certificate.from_openssl(openssl_cert)
+ cert.key_material.private_key = OpenSSL::PKey::RSA.new(key) # second argument is password, if set
+ return cert
+ end
+
+ def ca_root_signing_profile
+ {
+ "extensions" => {
+ "basicConstraints" => {"ca" => true},
+ "keyUsage" => {
+ "usage" => ["critical", "keyCertSign"]
+ },
+ "extendedKeyUsage" => {
+ "usage" => []
+ }
+ }
+ }
+ end
+
+ #
+ # for keyusage, openvpn server certs can have keyEncipherment or keyAgreement. I am not sure which is preferable.
+ # going with keyAgreement for now.
+ #
+ def server_signing_profile(node)
+ {
+ "extensions" => {
+ "keyUsage" => {
+ "usage" => ["digitalSignature", "keyAgreement"]
+ },
+ "extendedKeyUsage" => {
+ "usage" => ["serverAuth"]
+ },
+ "subjectAltName" => {
+ "uris" => [
+ "IP:#{node.ip_address}",
+ "DNS:#{node.domain.internal}"
+ ]
+ }
+ }
+ }
+ end
+
+ #
+ # For cert serial numbers, we need a non-colliding number less than 160 bits.
+ # md5 will do nicely, since there is no need for a secure hash, just a short one.
+ # (md5 is 128 bits)
+ #
+ def cert_serial_number(domain_name)
+ Digest::MD5.hexdigest("#{domain_name} -- #{Time.now}").to_i(16)
+ end
+
+end; end
diff --git a/lib/leap_cli/commands/node.rb b/lib/leap_cli/commands/node.rb
index e96293c..e200a19 100644
--- a/lib/leap_cli/commands/node.rb
+++ b/lib/leap_cli/commands/node.rb
@@ -8,7 +8,7 @@ module LeapCli; module Commands
##
desc 'not yet implemented... Create a new configuration for a node'
- command :'new-node' do |c|
+ command :'add-node' do |c|
c.action do |global_options,options,args|
end
end
diff --git a/lib/leap_cli/commands/project.rb b/lib/leap_cli/commands/project.rb
index 8ec9625..c748128 100644
--- a/lib/leap_cli/commands/project.rb
+++ b/lib/leap_cli/commands/project.rb
@@ -4,7 +4,7 @@ module LeapCli
desc 'Creates a new provider directory.'
arg_name '<directory>'
skips_pre
- command :'new-provider' do |c|
+ command :'init-provider' do |c|
c.action do |global_options,options,args|
directory = args.first
unless directory && directory.any?
diff --git a/lib/leap_cli/config/manager.rb b/lib/leap_cli/config/manager.rb
index 246b79f..72958dd 100644
--- a/lib/leap_cli/config/manager.rb
+++ b/lib/leap_cli/config/manager.rb
@@ -8,7 +8,7 @@ module LeapCli
#
class Manager
- attr_reader :services, :tags, :nodes, :provider
+ attr_reader :services, :tags, :nodes, :provider, :common
##
## IMPORT EXPORT
@@ -18,11 +18,11 @@ module LeapCli
# load .json configuration files
#
def load(provider_dir=Path.provider)
- @services = load_all_json(Path.named_path( [:service_config, '*'], provider_dir ))
- @tags = load_all_json(Path.named_path( [:tag_config, '*'], provider_dir ))
- @common = load_all_json(Path.named_path( :common_config, provider_dir ))['common']
- @provider = load_all_json(Path.named_path( :provider_config, provider_dir ))['provider']
- @nodes = load_all_json(Path.named_path( [:node_config, '*'], provider_dir ))
+ @services = load_all_json(Path.named_path([:service_config, '*'], provider_dir))
+ @tags = load_all_json(Path.named_path([:tag_config, '*'], provider_dir))
+ @nodes = load_all_json(Path.named_path([:node_config, '*'], provider_dir))
+ @common = load_json(Path.named_path(:common_config, provider_dir))
+ @provider = load_json(Path.named_path(:provider_config, provider_dir))
Util::assert!(@provider, "Failed to load provider.json")
Util::assert!(@common, "Failed to load common.json")
@@ -105,10 +105,10 @@ module LeapCli
private
- def load_all_json(pattern, config_type = :class)
+ def load_all_json(pattern)
results = Config::ObjectList.new
Dir.glob(pattern).each do |filename|
- obj = load_json(filename, config_type)
+ obj = load_json(filename)
if obj
name = File.basename(filename).sub(/\.json$/,'')
obj['name'] ||= name
@@ -118,9 +118,7 @@ module LeapCli
results
end
- def load_json(filename, config_type)
- #log2 { filename.sub(/^#{Regexp.escape(Path.root)}/,'') }
-
+ def load_json(filename)
#
# read file, strip out comments
# (File.read(filename) would be faster, but we like ability to have comments)
@@ -133,9 +131,8 @@ module LeapCli
end
end
- # parse json, and flatten hash
+ # parse json
begin
- #hash = Oj.load(buffer.string) || {}
hash = JSON.parse(buffer.string, :object_class => Hash, :array_class => Array) || {}
rescue SyntaxError => exc
log0 'Error in file "%s":' % filename
diff --git a/lib/leap_cli/config/object.rb b/lib/leap_cli/config/object.rb
index e044353..06a4fef 100644
--- a/lib/leap_cli/config/object.rb
+++ b/lib/leap_cli/config/object.rb
@@ -79,8 +79,6 @@ module LeapCli
end
elsif self.has_key?(key)
evaluate_value(key)
- elsif @node != self
- @node.get!(key)
else
raise NoMethodError.new(key, "No method '#{key}' for #{self.class}")
end
@@ -110,7 +108,7 @@ module LeapCli
#
def deep_merge!(object)
object.each do |key,new_value|
- old_value = self[key]
+ old_value = self.fetch key, nil
if old_value.is_a?(Hash) || new_value.is_a?(Hash)
# merge hashes
value = Config::Object.new(@manager, @node)
@@ -152,7 +150,7 @@ module LeapCli
else
if value =~ /^= (.*)$/
begin
- value = eval($1, self.send(:binding))
+ value = eval($1, @node.send(:binding))
self[key] = value
rescue SystemStackError => exc
puts "STACK OVERFLOW, BAILING OUT"
diff --git a/lib/leap_cli/path.rb b/lib/leap_cli/path.rb
index 9b4e3c9..aa20e17 100644
--- a/lib/leap_cli/path.rb
+++ b/lib/leap_cli/path.rb
@@ -23,7 +23,12 @@ module LeapCli; module Path
:hiera => 'hiera/#{arg}.yaml',
:node_ssh_pub_key => 'files/nodes/#{arg}/#{arg}_ssh_key.pub',
:known_hosts => 'files/ssh/known_hosts',
- :authorized_keys => 'files/ssh/authorized_keys'
+ :authorized_keys => 'files/ssh/authorized_keys',
+ :ca_key => 'files/ca/ca.key',
+ :ca_cert => 'files/ca/ca.crt',
+ :dh_params => 'files/ca/dh.pem',
+ :node_x509_key => 'files/nodes/#{arg}/#{arg}.key',
+ :node_x509_cert => 'files/nodes/#{arg}/#{arg}.crt'
}
#
@@ -132,7 +137,12 @@ module LeapCli; module Path
#
def self.named_path(name, provider_dir=Path.provider)
if name.is_a? Array
- name, arg = name
+ if name.length > 2
+ arg = name[1..-1]
+ name = name[0]
+ else
+ name, arg = name
+ end
else
arg = nil
end
diff --git a/lib/leap_cli/util.rb b/lib/leap_cli/util.rb
index 3b0c334..3bfb66b 100644
--- a/lib/leap_cli/util.rb
+++ b/lib/leap_cli/util.rb
@@ -67,6 +67,41 @@ module LeapCli
return output
end
+ def assert_files_missing!(*files)
+ options = files.last.is_a?(Hash) ? files.pop : {}
+ file_list = files.collect { |file_path|
+ file_path = Path.named_path(file_path)
+ File.exists?(file_path) ? relative_path(file_path) : nil
+ }.compact
+ if file_list.length > 1
+ bail! "Sorry, we can't continue because these files already exist: #{file_list.join(', ')}. You are not supposed to remove these files. Do so only with caution."
+ elsif file_list.length == 1
+ bail! "Sorry, we can't continue because this file already exists: #{file_list}. You are not supposed to remove this file. Do so only with caution."
+ end
+ end
+
+ def assert_config!(conf_path)
+ value = nil
+ begin
+ value = eval(conf_path, manager.send(:binding))
+ rescue NoMethodError
+ end
+ assert! value, "* Error: Nothing set for #{conf_path}"
+ end
+
+ def assert_files_exist!(*files)
+ options = files.last.is_a?(Hash) ? files.pop : {}
+ file_list = files.collect { |file_path|
+ file_path = Path.named_path(file_path)
+ !File.exists?(file_path) ? relative_path(file_path) : nil
+ }.compact
+ if file_list.length > 1
+ bail! "Sorry, you are missing these files: #{file_list.join(', ')}. #{options[:msg]}"
+ elsif file_list.length == 1
+ bail! "Sorry, you are missing this file: #{file_list.join(', ')}. #{options[:msg]}"
+ end
+ end
+
##
## FILES AND DIRECTORIES
##
@@ -176,14 +211,9 @@ module LeapCli
end
end
- #def rename_file(filepath)
- #end
-
- #private
-
- ##
- ## PRIVATE HELPER METHODS
- ##
+ def cmd_exists?(cmd)
+ `which #{cmd}`.strip.chars.any?
+ end
#
# compares md5 fingerprints to see if the contents of a file match the string we have in memory
@@ -198,6 +228,34 @@ module LeapCli
end
end
+ ##
+ ## PROCESSES
+ ##
+
+ #
+ # run a long running block of code in a separate process and display marching ants as time goes by.
+ # if the user hits ctrl-c, the program exits.
+ #
+ def long_running(&block)
+ pid = fork
+ if pid == nil
+ yield
+ exit!
+ end
+ Signal.trap("SIGINT") do
+ Process.kill("KILL", pid)
+ Process.wait(pid)
+ bail!
+ end
+ while true
+ sleep 0.2
+ STDOUT.print '.'
+ STDOUT.flush
+ break if Process.wait(pid, Process::WNOHANG)
+ end
+ STDOUT.puts
+ end
+
end
end
diff --git a/lib/leap_cli/version.rb b/lib/leap_cli/version.rb
index 366e5a2..437d861 100644
--- a/lib/leap_cli/version.rb
+++ b/lib/leap_cli/version.rb
@@ -2,6 +2,6 @@ module LeapCli
unless defined?(LeapCli::VERSION)
VERSION = '0.1.0'
SUMMARY = 'Command line interface to the LEAP platform'
- DESCRIPTION = 'Provides the command "leap", used to manage a bevy of servers running the LEAP platform from the comfort of your own home.'
+ DESCRIPTION = 'The command "leap" can be used to manage a bevy of servers running the LEAP platform from the comfort of your own home.'
end
end
diff --git a/test/provider/common.json b/test/provider/common.json
index 8f83558..9e19836 100644
--- a/test/provider/common.json
+++ b/test/provider/common.json
@@ -17,4 +17,9 @@
"known_hosts": "= file :known_hosts",
"port": 22
}
+ #"x509": {
+ # "use": false,
+ # "cert": "= x509.use ? file(:node_x509_cert) : nil",
+ # "key": "= x509.use ? file(:node_x509_key) : nil"
+ #}
}
diff --git a/test/provider/provider.json b/test/provider/provider.json
index 4e8bb34..d4153a6 100644
--- a/test/provider/provider.json
+++ b/test/provider/provider.json
@@ -13,7 +13,12 @@
"enrollment_policy": "open",
"ca": {
"name": "Rewire Root CA",
- "organization": "#{name}",
- "bit_size": 4096
+ "organization": "= global.provider.name[global.provider.default_language]",
+ "organizational_unit": "= 'https://' + global.common.domain.full_suffix",
+ "bit_size": 4096,
+ "server_certificates": {
+ "bit_size": 3248,
+ "life_span": "1y"
+ }
}
} \ No newline at end of file
diff --git a/test/provider/services/openvpn.json b/test/provider/services/openvpn.json
index 86d6c14..629c5b7 100644
--- a/test/provider/services/openvpn.json
+++ b/test/provider/services/openvpn.json
@@ -5,9 +5,12 @@
"nat": true,
"ca_crt": "= file 'ca/ca.crt'",
"ca_key": "= file 'ca/ca.key'",
- "dh_key": "= file 'ca/dh.key'",
+ "dh": "= file 'ca/dh.pem'",
"server_crt": "= file domain.name + '.crt'",
"server_key": "= file domain.name + '.key'"
},
"service_type": "user_service"
+ #"x509": {
+ # "use": true
+ #}
}