summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/leap_cli/commands/ca.rb493
-rw-r--r--lib/leap_cli/commands/node.rb157
-rw-r--r--lib/leap_cli/config/node.rb176
-rw-r--r--lib/leap_cli/config/node_cert.rb124
-rw-r--r--lib/leap_cli/util/x509.rb33
-rw-r--r--lib/leap_cli/x509.rb16
-rw-r--r--lib/leap_cli/x509/certs.rb232
-rw-r--r--lib/leap_cli/x509/signing_profiles.rb104
-rw-r--r--lib/leap_cli/x509/utils.rb26
9 files changed, 769 insertions, 592 deletions
diff --git a/lib/leap_cli/commands/ca.rb b/lib/leap_cli/commands/ca.rb
index 6abffe7b..f998d0fe 100644
--- a/lib/leap_cli/commands/ca.rb
+++ b/lib/leap_cli/commands/ca.rb
@@ -1,8 +1,3 @@
-autoload :OpenSSL, 'openssl'
-autoload :CertificateAuthority, 'certificate_authority'
-autoload :Date, 'date'
-require 'digest/md5'
-
module LeapCli; module Commands
desc "Manage X.509 certificates"
@@ -35,37 +30,10 @@ module LeapCli; module Commands
cert.desc 'Creates a Diffie-Hellman parameter file, needed for forward secret OpenVPN ciphers.' # (needed for server-side of some TLS connections)
cert.command :dh do |dh|
dh.action do |global_options,options,args|
- long_running do
- if cmd_exists?('certtool')
- log 0, 'Generating DH parameters (takes a long time)...'
- output = assert_run!('certtool --generate-dh-params --sec-param high')
- output.sub!(/.*(-----BEGIN DH PARAMETERS-----.*-----END DH PARAMETERS-----).*/m, '\1')
- output << "\n"
- write_file!(:dh_params, output)
- else
- log 0, 'Generating DH parameters (takes a REALLY long time)...'
- output = OpenSSL::PKey::DH.generate(3248).to_pem
- write_file!(:dh_params, output)
- end
- end
+ generate_dh
end
end
- #
- # hints:
- #
- # inspect CSR:
- # openssl req -noout -text -in files/cert/x.csr
- #
- # generate CSR with openssl to see how it compares:
- # openssl req -sha256 -nodes -newkey rsa:2048 -keyout example.key -out example.csr
- #
- # validate a CSR:
- # http://certlogik.com/decoder/
- #
- # nice details about CSRs:
- # http://www.redkestrel.co.uk/Articles/CSR.html
- #
cert.desc "Creates a CSR for use in buying a commercial X.509 certificate."
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`, "+
@@ -81,67 +49,7 @@ module LeapCli; module Commands
csr.flag :bits, :arg_name => 'BITS', :desc => "Override default certificate bit length"
csr.flag :digest, :arg_name => 'DIGEST', :desc => "Override default signature digest"
csr.action do |global_options,options,args|
- assert_config! 'provider.domain'
- assert_config! 'provider.name'
- assert_config! 'provider.default_language'
- assert_config! 'provider.ca.server_certificates.bit_size'
- assert_config! 'provider.ca.server_certificates.digest'
- domain = options[:domain] || provider.domain
-
- unless global_options[:force]
- 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 or run with --force.'
- end
-
- server_certificates = provider.ca.server_certificates
-
- # RSA key
- keypair = CertificateAuthority::MemoryKeyMaterial.new
- bit_size = (options[:bits] || server_certificates.bit_size).to_i
- log :generating, "%s bit RSA key" % bit_size do
- keypair.generate_key(bit_size)
- write_file! [:commercial_key, domain], keypair.private_key.to_pem
- end
-
- # CSR
- dn = CertificateAuthority::DistinguishedName.new
- dn.common_name = domain
- dn.organization = options[:organization] || provider.name[provider.default_language]
- dn.ou = options[:organizational_unit] # optional
- dn.email_address = options[:email] # optional
- dn.country = options[:country] || server_certificates['country'] # optional
- dn.state = options[:state] || server_certificates['state'] # optional
- dn.locality = options[:locality] || server_certificates['locality'] # optional
-
- digest = options[:digest] || server_certificates.digest
- log :generating, "CSR with #{digest} digest and #{print_dn(dn)}" do
- csr = create_csr(dn, keypair, digest)
- write_file! [:commercial_csr, domain], csr.to_pem
- end
-
- # Sign using our own CA, for use in testing but hopefully not production.
- # It is not that commerical CAs are so secure, it is just that signing your own certs is
- # a total drag for the user because they must click through dire warnings.
- #if options[:sign]
- log :generating, "self-signed x509 server certificate for testing purposes" do
- cert = csr.to_cert
- cert.serial_number.number = cert_serial_number(domain)
- cert.not_before = yesterday
- cert.not_after = yesterday.advance(:years => 1)
- cert.parent = ca_root
- cert.sign! domain_test_signing_profile
- write_file! [:commercial_cert, domain], cert.to_pem
- log "please replace this file with the real certificate you get from a CA using #{Path.relative_path([:commercial_csr, domain])}"
- end
- #end
-
- # FAKE CA
- unless file_exists? :commercial_ca_cert
- log :using, "generated CA in place of commercial CA for testing purposes" do
- write_file! :commercial_ca_cert, read_file!(:ca_cert)
- log "please also replace this file with the CA cert from the commercial authority you use."
- end
- end
+ generate_csr(global_options, options, args)
end
end
end
@@ -152,6 +60,7 @@ module LeapCli; module Commands
# will generate new certificates for the specified nodes, if needed.
#
def update_certificates(nodes, options={})
+ require 'leap_cli/x509'
assert_files_exist! :ca_cert, :ca_key, :msg => 'Run `leap cert ca` to create them'
assert_config! 'provider.ca.server_certificates.bit_size'
assert_config! 'provider.ca.server_certificates.digest'
@@ -159,382 +68,102 @@ module LeapCli; module Commands
assert_config! 'common.x509.use'
nodes.each_node do |node|
- warn_if_commercial_cert_will_soon_expire(node)
+ node.warn_if_commercial_cert_will_soon_expire
if !node.x509.use
remove_file!([:node_x509_key, node.name])
remove_file!([:node_x509_cert, node.name])
- elsif options[:force] || cert_needs_updating?(node)
- generate_cert_for_node(node)
- end
- end
- end
-
- private
-
- def generate_new_certificate_authority(key_file, cert_file, common_name)
- assert_files_missing! key_file, cert_file
- assert_config! 'provider.ca.name'
- assert_config! 'provider.ca.bit_size'
- assert_config! 'provider.ca.life_span'
-
- root = CertificateAuthority::Certificate.new
-
- # set subject
- root.subject.common_name = common_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
- root.not_before = yesterday
- root.not_after = yesterday_advance(provider.ca.life_span)
-
- # 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!(key_file, root.key_material.private_key.to_pem)
- write_file!(cert_file, root.to_pem)
- end
-
- #
- # returns true if the certs associated with +node+ need to be regenerated.
- #
- def cert_needs_updating?(node)
- if !file_exists?([:node_x509_cert, node.name], [:node_x509_key, node.name])
- return true
- else
- cert = load_certificate_file([:node_x509_cert, node.name])
- if !created_by_authority?(cert, ca_root)
- log :updating, "cert for node '#{node.name}' because it was signed by an old CA root cert."
- return true
- end
- if cert.not_after < Time.now.advance(:months => 2)
- log :updating, "cert for node '#{node.name}' because it will expire soon"
- return true
- end
- if cert.subject.common_name != node.domain.full
- log :updating, "cert for node '#{node.name}' because domain.full has changed (was #{cert.subject.common_name}, now #{node.domain.full})"
- return true
- end
- cert.openssl_body.extensions.each do |ext|
- if ext.oid == "subjectAltName"
- ips = []
- dns_names = []
- ext.value.split(",").each do |value|
- value.strip!
- ips << $1 if value =~ /^IP Address:(.*)$/
- dns_names << $1 if value =~ /^DNS:(.*)$/
- end
- dns_names.sort!
- if ips.first != node.ip_address
- log :updating, "cert for node '#{node.name}' because ip_address has changed (from #{ips.first} to #{node.ip_address})"
- return true
- elsif dns_names != dns_names_for_node(node)
- log :updating, "cert for node '#{node.name}' because domain name aliases have changed\n from: #{dns_names.inspect}\n to: #{dns_names_for_node(node).inspect})"
- return true
- end
- end
+ elsif options[:force] || node.cert_needs_updating?
+ node.generate_cert
end
end
- return false
- end
-
- def created_by_authority?(cert, ca)
- authority_key_id = cert.extensions["authorityKeyIdentifier"].identifier.sub(/^keyid:/, '')
- authority_key_id == public_key_id_for_ca(ca)
- end
-
- # calculate the "key id" for a root CA, that matches the value
- # Authority Key Identifier in the x509 extensions of a cert.
- def public_key_id_for_ca(ca_cert)
- @ca_key_ids ||= {}
- @ca_key_ids[ca_cert.object_id] ||= begin
- pubkey = ca_cert.key_material.public_key
- seq = OpenSSL::ASN1::Sequence([
- OpenSSL::ASN1::Integer.new(pubkey.n),
- OpenSSL::ASN1::Integer.new(pubkey.e)
- ])
- Digest::SHA1.hexdigest(seq.to_der).upcase.scan(/../).join(':')
- end
- end
-
- def warn_if_commercial_cert_will_soon_expire(node)
- dns_names_for_node(node).each do |domain|
- if file_exists?([:commercial_cert, domain])
- cert = load_certificate_file([:commercial_cert, domain])
- path = Path.relative_path([:commercial_cert, domain])
- if cert.not_after < Time.now.utc
- log :error, "the commercial certificate '#{path}' has EXPIRED! " +
- "You should renew it with `leap cert csr --domain #{domain}`."
- elsif cert.not_after < Time.now.advance(:months => 2)
- log :warning, "the commercial certificate '#{path}' will expire soon (#{cert.not_after}). "+
- "You should renew it with `leap cert csr --domain #{domain}`."
- end
- end
- end
- end
-
- def generate_cert_for_node(node)
- return if node.x509.use == false
-
- cert = CertificateAuthority::Certificate.new
-
- # set subject
- cert.subject.common_name = node.domain.full
- cert.serial_number.number = cert_serial_number(node.domain.full)
-
- # set expiration
- cert.not_before = yesterday
- cert.not_after = yesterday_advance(provider.ca.server_certificates.life_span)
-
- # generate key
- 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
#
# yields client key and cert suitable for testing
#
def generate_test_client_cert(prefix=nil)
+ require 'leap_cli/x509'
cert = CertificateAuthority::Certificate.new
cert.serial_number.number = cert_serial_number(provider.domain)
cert.subject.common_name = [prefix, random_common_name(provider.domain)].join
- cert.not_before = yesterday
- cert.not_after = yesterday.advance(:years => 1)
+ cert.not_before = X509.yesterday
+ cert.not_after = X509.yesterday.advance(:years => 1)
cert.key_material.generate_key(1024) # just for testing, remember!
cert.parent = client_ca_root
cert.sign! client_test_signing_profile
yield cert.key_material.private_key.to_pem, cert.to_pem
end
- #
- # creates a CSR and returns it.
- # with the correct extReq attribute so that the CA
- # doens't generate certs with extensions we don't want.
- #
- def create_csr(dn, keypair, digest)
- csr = CertificateAuthority::SigningRequest.new
- csr.distinguished_name = dn
- csr.key_material = keypair
- csr.digest = digest
-
- # define extensions manually (library doesn't support setting these on CSRs)
- extensions = []
- extensions << CertificateAuthority::Extensions::BasicConstraints.new.tap {|basic|
- basic.ca = false
- }
- extensions << CertificateAuthority::Extensions::KeyUsage.new.tap {|keyusage|
- keyusage.usage = ["digitalSignature", "keyEncipherment"]
- }
- extensions << CertificateAuthority::Extensions::ExtendedKeyUsage.new.tap {|extkeyusage|
- extkeyusage.usage = [ "serverAuth"]
- }
-
- # convert extensions to attribute 'extReq'
- # aka "Requested Extensions"
- factory = OpenSSL::X509::ExtensionFactory.new
- attrval = OpenSSL::ASN1::Set([OpenSSL::ASN1::Sequence(
- extensions.map{|e| factory.create_ext(e.openssl_identifier, e.to_s, e.critical)}
- )])
- attrs = [
- OpenSSL::X509::Attribute.new("extReq", attrval),
- ]
- csr.attributes = attrs
+ private
- return csr
- end
+ def generate_new_certificate_authority(key_file, cert_file, common_name)
+ require 'leap_cli/x509'
+ assert_files_missing! key_file, cert_file
+ assert_config! 'provider.ca.name'
+ assert_config! 'provider.ca.bit_size'
+ assert_config! 'provider.ca.life_span'
- def ca_root
- @ca_root ||= begin
- load_certificate_file(:ca_cert, :ca_key)
- end
- end
+ root = X509.new_ca(provider.ca, common_name)
- def client_ca_root
- @client_ca_root ||= begin
- load_certificate_file(:client_ca_cert, :client_ca_key)
- end
+ write_file!(key_file, root.key_material.private_key.to_pem)
+ write_file!(cert_file, root.to_pem)
end
- def load_certificate_file(crt_file, key_file=nil, password=nil)
- crt = read_file!(crt_file)
- openssl_cert = OpenSSL::X509::Certificate.new(crt)
- cert = CertificateAuthority::Certificate.from_openssl(openssl_cert)
- if key_file
- key = read_file!(key_file)
- cert.key_material.private_key = OpenSSL::PKey::RSA.new(key, password)
+ def generate_dh
+ require 'leap_cli/x509'
+ long_running do
+ if cmd_exists?('certtool')
+ log 0, 'Generating DH parameters (takes a long time)...'
+ output = assert_run!('certtool --generate-dh-params --sec-param high')
+ output.sub!(/.*(-----BEGIN DH PARAMETERS-----.*-----END DH PARAMETERS-----).*/m, '\1')
+ output << "\n"
+ write_file!(:dh_params, output)
+ else
+ log 0, 'Generating DH parameters (takes a REALLY long time)...'
+ output = OpenSSL::PKey::DH.generate(3248).to_pem
+ write_file!(:dh_params, output)
+ end
end
- 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.
- # Web browsers seem to break without keyEncipherment.
- # For now, I am using digitalSignature + keyEncipherment
- #
- # * digitalSignature -- for (EC)DHE cipher suites
- # "The digitalSignature bit is asserted when the subject public key is used
- # with a digital signature mechanism to support security services other
- # than certificate signing (bit 5), or CRL signing (bit 6). Digital
- # signature mechanisms are often used for entity authentication and data
- # origin authentication with integrity."
- #
- # * keyEncipherment ==> for plain RSA cipher suites
- # "The keyEncipherment bit is asserted when the subject public key is used for
- # key transport. For example, when an RSA key is to be used for key management,
- # then this bit is set."
- #
- # * keyAgreement ==> for used with DH, not RSA.
- # "The keyAgreement bit is asserted when the subject public key is used for key
- # agreement. For example, when a Diffie-Hellman key is to be used for key
- # management, then this bit is set."
- #
- # digest options: SHA512, SHA256, SHA1
- #
- def server_signing_profile(node)
- {
- "digest" => provider.ca.server_certificates.digest,
- "extensions" => {
- "keyUsage" => {
- "usage" => ["digitalSignature", "keyEncipherment"]
- },
- "extendedKeyUsage" => {
- "usage" => ["serverAuth", "clientAuth"]
- },
- "subjectAltName" => {
- "ips" => [node.ip_address],
- "dns_names" => dns_names_for_node(node)
- }
- }
- }
end
#
- # This is used when signing the main cert for the provider's domain
- # with our own CA (for testing purposes). Typically, this cert would
- # be purchased from a commercial CA, and not signed this way.
+ # hints:
#
- def domain_test_signing_profile
- {
- "digest" => "SHA256",
- "extensions" => {
- "keyUsage" => {
- "usage" => ["digitalSignature", "keyEncipherment"]
- },
- "extendedKeyUsage" => {
- "usage" => ["serverAuth"]
- }
- }
- }
- end
-
+ # inspect CSR:
+ # openssl req -noout -text -in files/cert/x.csr
#
- # This is used when signing a dummy client certificate that is only to be
- # used for testing.
+ # generate CSR with openssl to see how it compares:
+ # openssl req -sha256 -nodes -newkey rsa:2048 -keyout example.key -out example.csr
#
- def client_test_signing_profile
- {
- "digest" => "SHA256",
- "extensions" => {
- "keyUsage" => {
- "usage" => ["digitalSignature"]
- },
- "extendedKeyUsage" => {
- "usage" => ["clientAuth"]
- }
- }
- }
- end
-
- def dns_names_for_node(node)
- names = [node.domain.internal, node.domain.full]
- if node['dns'] && node.dns['aliases'] && node.dns.aliases.any?
- names += node.dns.aliases
- end
- names.compact!
- names.sort!
- names.uniq!
- return names
- end
-
+ # validate a CSR:
+ # http://certlogik.com/decoder/
#
- # 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)
+ # nice details about CSRs:
+ # http://www.redkestrel.co.uk/Articles/CSR.html
#
- def cert_serial_number(domain_name)
- Digest::MD5.hexdigest("#{domain_name} -- #{Time.now}").to_i(16)
- end
+ def generate_csr(global_options, options, args)
+ require 'leap_cli/x509'
+ assert_config! 'provider.domain'
+ assert_config! 'provider.name'
+ assert_config! 'provider.default_language'
+ assert_config! 'provider.ca.server_certificates.bit_size'
+ assert_config! 'provider.ca.server_certificates.digest'
- #
- # for the random common name, we need a text string that will be unique across all certs.
- # ruby 1.8 doesn't have a built-in uuid generator, or we would use SecureRandom.uuid
- #
- def random_common_name(domain_name)
- cert_serial_number(domain_name).to_s(36)
- end
+ server_certificates = provider.ca.server_certificates
+ options[:domain] ||= provider.domain
+ options[:organization] ||= provider.name[provider.default_language]
+ options[:country] ||= server_certificates['country']
+ options[:state] ||= server_certificates['state']
+ options[:locality] ||= server_certificates['locality']
+ options[:bits] ||= server_certificates.bit_size
+ options[:digest] ||= server_certificates.digest
- # prints CertificateAuthority::DistinguishedName fields
- def print_dn(dn)
- fields = {}
- [:common_name, :locality, :state, :country, :organization, :organizational_unit, :email_address].each do |attr|
- fields[attr] = dn.send(attr) if dn.send(attr)
+ unless global_options[:force]
+ assert_files_missing! [:commercial_key, options[:domain]], [:commercial_csr, options[:domain]],
+ :msg => 'If you really want to create a new key and CSR, remove these files first or run with --force.'
end
- fields.inspect
- end
-
- ##
- ## TIME HELPERS
- ##
- ## note: we use 'yesterday' instead of 'today', because times are in UTC, and some people on the planet
- ## are behind UTC.
- ##
-
- def yesterday
- t = Time.now - 24*24*60
- Time.utc t.year, t.month, t.day
- end
- def yesterday_advance(string)
- number, unit = string.split(' ')
- unless ['years', 'months', 'days', 'hours', 'minutes'].include? unit
- bail!("The time property '#{string}' is missing a unit (one of: years, months, days, hours, minutes).")
- end
- unless number.to_i.to_s == number
- bail!("The time property '#{string}' is missing a number.")
- end
- yesterday.advance(unit.to_sym => number.to_i)
+ X509.create_csr_and_cert(options)
end
end; end
diff --git a/lib/leap_cli/commands/node.rb b/lib/leap_cli/commands/node.rb
index 1dce437e..9d210244 100644
--- a/lib/leap_cli/commands/node.rb
+++ b/lib/leap_cli/commands/node.rb
@@ -3,8 +3,6 @@
# but all other `node x` commands live here.
#
-autoload :IPAddr, 'ipaddr'
-
module LeapCli; module Commands
##
@@ -23,29 +21,7 @@ module LeapCli; module Commands
node.command :add do |add|
add.switch :local, :desc => 'Make a local testing node (by automatically assigning the next available local IP address). Local nodes are run as virtual machines on your computer.', :negatable => false
add.action do |global_options,options,args|
- # argument sanity checks
- name = args.first
- assert_valid_node_name!(name, options[:local])
- assert_files_missing! [:node_config, name]
-
- # create and seed new node
- node = Config::Node.new(manager.env)
- if options[:local]
- node['ip_address'] = pick_next_vagrant_ip_address
- end
- seed_node_data_from_cmd_line(node, args[1..-1])
- seed_node_data_from_template(node)
- validate_ip_address(node)
- begin
- node['name'] = name
- json = node.dump_json(:exclude => ['name'])
- write_file!([:node_config, name], json + "\n")
- if file_exists? :ca_cert, :ca_key
- generate_cert_for_node(manager.reload_node!(node))
- end
- rescue LeapCli::ConfigError
- remove_node_files(name)
- end
+ add_node(global_options, options, args)
end
end
@@ -53,15 +29,7 @@ module LeapCli; module Commands
node.arg_name 'OLD_NAME NEW_NAME'
node.command :mv do |mv|
mv.action do |global_options,options,args|
- node = get_node_from_args(args, include_disabled: true)
- new_name = args.last
- assert_valid_node_name!(new_name, node.vagrant?)
- ensure_dir [:node_files_dir, new_name]
- Leap::Platform.node_files.each do |path|
- rename_file! [path, node.name], [path, new_name]
- end
- remove_directory! [:node_files_dir, node.name]
- rename_node_facts(node.name, new_name)
+ move_node(global_options, options, args)
end
end
@@ -69,12 +37,7 @@ module LeapCli; module Commands
node.arg_name 'NAME' #:optional => false #, :multiple => false
node.command :rm do |rm|
rm.action do |global_options,options,args|
- node = get_node_from_args(args, include_disabled: true)
- remove_node_files(node.name)
- if node.vagrant?
- vagrant_command("destroy --force", [node.name])
- end
- remove_node_facts(node.name)
+ rm_node(global_options, options, args)
end
end
end
@@ -93,96 +56,50 @@ module LeapCli; module Commands
node
end
- def seed_node_data_from_cmd_line(node, args)
- args.each do |seed|
- key, value = seed.split(':', 2)
- value = format_seed_value(value)
- assert! key =~ /^[0-9a-z\._]+$/, "illegal characters used in property '#{key}'"
- if key =~ /\./
- key_parts = key.split('.')
- final_key = key_parts.pop
- current_object = node
- key_parts.each do |key_part|
- current_object[key_part] ||= Config::Object.new
- current_object = current_object[key_part]
- end
- current_object[final_key] = value
- else
- node[key] = value
- end
- end
- end
+ protected
- #
- # load "new node template" information into the `node`, modifying `node`.
- # values in the template will not override existing node values.
- #
- def seed_node_data_from_template(node)
- node.inherit_from!(manager.template('common'))
- [node['services']].flatten.each do |service|
- if service
- template = manager.template(service)
- if template
- node.inherit_from!(template)
- end
- end
+ def add_node(global, options, args)
+ name = args.first
+ unless global[:force]
+ assert_files_missing! [:node_config, name]
end
- end
-
- def remove_node_files(node_name)
- (Leap::Platform.node_files + [:node_files_dir]).each do |path|
- remove_file! [path, node_name]
+ node = Config::Node.new(manager.env)
+ node['name'] = name
+ if options[:ip_address]
+ node['ip_address'] = options[:ip_address]
+ elsif options[:local]
+ node['ip_address'] = pick_next_vagrant_ip_address
end
+ node.seed_from_args(args[1..-1])
+ node.seed_from_template
+ node.validate!
+ node.write_configs
+ # reapply inheritance, since tags/services might have changed:
+ node = manager.reload_node!(node)
+ node.generate_cert
end
- #
- # conversions:
- #
- # "x,y,z" => ["x","y","z"]
- #
- # "22" => 22
- #
- # "5.1" => 5.1
- #
- def format_seed_value(v)
- if v =~ /,/
- v = v.split(',')
- v.map! do |i|
- i = i.to_i if i.to_i.to_s == i
- i = i.to_f if i.to_f.to_s == i
- i
- end
- else
- v = v.to_i if v.to_i.to_s == v
- v = v.to_f if v.to_f.to_s == v
- end
- return v
- end
+ private
- def validate_ip_address(node)
- if node['ip_address'] == "REQUIRED"
- bail! do
- log :error, "ip_address is not set. Specify with `leap node add NAME ip_address:ADDRESS`."
- end
- end
- IPAddr.new(node['ip_address'])
- rescue ArgumentError
- bail! do
- if node['ip_address']
- log :invalid, "ip_address #{node['ip_address'].inspect}"
- else
- log :missing, "ip_address"
- end
+ def move_node(global, options, args)
+ node = get_node_from_args(args, include_disabled: true)
+ new_name = args.last
+ Config::Node.validate_name!(new_name, node.vagrant?)
+ ensure_dir [:node_files_dir, new_name]
+ Leap::Platform.node_files.each do |path|
+ rename_file! [path, node.name], [path, new_name]
end
+ remove_directory! [:node_files_dir, node.name]
+ rename_node_facts(node.name, new_name)
end
- def assert_valid_node_name!(name, local=false)
- assert! name, 'No <node-name> specified.'
- if local
- assert! name =~ /^[0-9a-z]+$/, "illegal characters used in node name '#{name}' (note: Vagrant does not allow hyphens or underscores)"
- else
- assert! name =~ /^[0-9a-z-]+$/, "illegal characters used in node name '#{name}' (note: Linux does not allow underscores)"
+ def rm_node(global, options, args)
+ node = get_node_from_args(args, include_disabled: true)
+ node.remove_files
+ if node.vagrant?
+ vagrant_command("destroy --force", [node.name])
end
+ remove_node_facts(node.name)
end
end; end
diff --git a/lib/leap_cli/config/node.rb b/lib/leap_cli/config/node.rb
index f8ec0527..2d76b814 100644
--- a/lib/leap_cli/config/node.rb
+++ b/lib/leap_cli/config/node.rb
@@ -9,8 +9,11 @@ module LeapCli; module Config
class Node < Object
attr_accessor :file_paths
- def initialize(environment=nil)
+ def initialize(environment=nil) #, name=nil)
super(environment)
+ #if name
+ # self['name'] = name
+ #end
@node = self
@file_paths = []
end
@@ -19,18 +22,20 @@ module LeapCli; module Config
# returns true if this node has an ip address in the range of the vagrant network
#
def vagrant?
+ ip = self['ip_address']
+ return false unless ip
begin
vagrant_range = IPAddr.new LeapCli.leapfile.vagrant_network
- rescue ArgumentError => exc
- Util::bail! { Util::log :invalid, "ip address '#{@node.ip_address}' vagrant.network" }
+ rescue ArgumentError
+ Util::bail! { Util::log :invalid, "vagrant_network in Leapfile or .leaprc" }
end
begin
- ip_address = IPAddr.new @node.get('ip_address')
- rescue ArgumentError => exc
- Util::log :warning, "invalid ip address '#{@node.get('ip_address')}' for node '#{@node.name}'"
+ ip_addr = IPAddr.new(ip)
+ rescue ArgumentError
+ Util::log :warning, "invalid ip address '#{ip}' for node '#{@node.name}'"
end
- return vagrant_range.include?(ip_address)
+ return vagrant_range.include?(ip_addr)
end
#
@@ -73,6 +78,163 @@ module LeapCli; module Config
)
end
+ #
+ # Takes strings such as "openvpn.gateway_address:1.1.1.1"
+ # and converts this to data stored in this node.
+ #
+ def seed_from_args(args)
+ args.each do |seed|
+ key, value = seed.split(':', 2)
+ value = format_seed_value(value)
+ Util.assert! key =~ /^[0-9a-z\._]+$/, "illegal characters used in property '#{key}'"
+ if key =~ /\./
+ key_parts = key.split('.')
+ final_key = key_parts.pop
+ current_object = self
+ key_parts.each do |key_part|
+ current_object[key_part] ||= Config::Object.new
+ current_object = current_object[key_part]
+ end
+ current_object[final_key] = value
+ else
+ self[key] = value
+ end
+ end
+ end
+
+ #
+ # Seeds values for this node from a template, based on the services.
+ # Values in the template will not override existing node values.
+ #
+ def seed_from_template
+ inherit_from!(manager.template('common'))
+ [self['services']].flatten.each do |service|
+ if service
+ template = manager.template(service)
+ if template
+ inherit_from!(template)
+ end
+ end
+ end
+ end
+
+ #
+ # bails if the node is not valid.
+ #
+ def validate!
+ #
+ # validate ip_address
+ #
+ if self['ip_address'] == "REQUIRED"
+ Util.bail! do
+ Util.log :error, "ip_address is not set. " +
+ "Specify with `leap node add NAME ip_address:ADDRESS`."
+ end
+ elsif self['ip_address']
+ begin
+ IPAddr.new(self['ip_address'])
+ rescue ArgumentError
+ Util.bail! do
+ Util.log :invalid, "ip_address #{self['ip_address'].inspect}"
+ end
+ end
+ end
+
+ #
+ # validate name
+ #
+ self.class.validate_name!(self.name, self.vagrant?)
+ end
+
+ #
+ # create or update all the configs needed for this node,
+ # including x.509 certs as needed.
+ #
+ # note: this method will write to disk EVERYTHING
+ # in the node, which is not what you want
+ # if the node has inheritance applied.
+ #
+ def write_configs
+ json = self.dump_json(:exclude => ['name'])
+ Util.write_file!([:node_config, name], json + "\n")
+ rescue LeapCli::ConfigError
+ Config::Node.remove_node_files(self.name)
+ end
+
+ #
+ # modifies the config file nodes/NAME.json for this node.
+ #
+ def update_json(new_values)
+ self.env.update_node_json(node, new_values)
+ end
+
+ #
+ # returns an array of all possible dns names for this node
+ #
+ def all_dns_names
+ names = [@node.domain.internal, @node.domain.full]
+ if @node['dns'] && @node.dns['aliases'] && @node.dns.aliases.any?
+ names += @node.dns.aliases
+ end
+ names.compact!
+ names.sort!
+ names.uniq!
+ return names
+ end
+
+ def remove_files
+ self.class.remove_node_files(self.name)
+ end
+
+ ##
+ ## Class Methods
+ ##
+
+ def self.remove_node_files(node_name)
+ (Leap::Platform.node_files + [:node_files_dir]).each do |path|
+ Util.remove_file! [path, node_name]
+ end
+ end
+
+ def self.validate_name!(name, local=false)
+ Util.assert! name, 'Node is missing a name.'
+ if local
+ Util.assert! name =~ /^[0-9a-z]+$/,
+ "illegal characters used in node name '#{name}' " +
+ "(note: Vagrant does not allow hyphens or underscores)"
+ else
+ Util.assert! name =~ /^[0-9a-z-]+$/,
+ "illegal characters used in node name '#{name}' " +
+ "(note: Linux does not allow underscores)"
+ end
+ end
+
+ private
+
+ #
+ # conversions:
+ #
+ # "x,y,z" => ["x","y","z"]
+ #
+ # "22" => 22
+ #
+ # "5.1" => 5.1
+ #
+ def format_seed_value(v)
+ if v =~ /,/
+ v = v.split(',')
+ v.map! do |i|
+ i = i.to_i if i.to_i.to_s == i
+ i = i.to_f if i.to_f.to_s == i
+ i
+ end
+ else
+ v = v.to_i if v.to_i.to_s == v
+ v = v.to_f if v.to_f.to_s == v
+ end
+ return v
+ end
+
end
end; end
diff --git a/lib/leap_cli/config/node_cert.rb b/lib/leap_cli/config/node_cert.rb
new file mode 100644
index 00000000..64842ffa
--- /dev/null
+++ b/lib/leap_cli/config/node_cert.rb
@@ -0,0 +1,124 @@
+#
+# x509 related methods for Config::Node
+#
+module LeapCli; module Config
+
+ class Node < Object
+
+ #
+ # creates a new server certificate file for this node
+ #
+ def generate_cert
+ require 'leap_cli/x509'
+
+ if self['x509.use'] == false ||
+ !Util.file_exists?(:ca_cert, :ca_key) ||
+ !self.cert_needs_updating?
+ return false
+ end
+
+ cert = CertificateAuthority::Certificate.new
+ provider = env.provider
+
+ # set subject
+ cert.subject.common_name = self.domain.full
+ cert.serial_number.number = X509.cert_serial_number(self.domain.full)
+
+ # set expiration
+ cert.not_before = X509.yesterday
+ cert.not_after = X509.yesterday_advance(provider.ca.server_certificates.life_span)
+
+ # generate key
+ cert.key_material.generate_key(provider.ca.server_certificates.bit_size)
+
+ # sign
+ cert.parent = X509.ca_root
+ cert.sign!(X509.server_signing_profile(self))
+
+ # save
+ Util.write_file!([:node_x509_key, self.name], cert.key_material.private_key.to_pem)
+ Util.write_file!([:node_x509_cert, self.name], cert.to_pem)
+ end
+
+ #
+ # returns true if the certs associated with +node+ need to be regenerated.
+ #
+ def cert_needs_updating?(log_comments=true)
+ require 'leap_cli/x509'
+
+ if log_comments
+ def log(*args, &block)
+ Util.log(*args, &block)
+ end
+ else
+ def log(*args); end
+ end
+
+ node = self
+ if !Util.file_exists?([:node_x509_cert, node.name], [:node_x509_key, node.name])
+ return true
+ else
+ cert = X509.load_certificate_file([:node_x509_cert, node.name])
+ if !X509.created_by_authority?(cert)
+ log :updating, "cert for node '#{node.name}' because it was signed by an old CA root cert."
+ return true
+ end
+ if cert.not_after < Time.now.advance(:months => 2)
+ log :updating, "cert for node '#{node.name}' because it will expire soon"
+ return true
+ end
+ if cert.subject.common_name != node.domain.full
+ log :updating, "cert for node '#{node.name}' because domain.full has changed (was #{cert.subject.common_name}, now #{node.domain.full})"
+ return true
+ end
+ cert.openssl_body.extensions.each do |ext|
+ if ext.oid == "subjectAltName"
+ ips = []
+ dns_names = []
+ ext.value.split(",").each do |value|
+ value.strip!
+ ips << $1 if value =~ /^IP Address:(.*)$/
+ dns_names << $1 if value =~ /^DNS:(.*)$/
+ end
+ dns_names.sort!
+ if ips.first != node.ip_address
+ log :updating, "cert for node '#{node.name}' because ip_address has changed (from #{ips.first} to #{node.ip_address})"
+ return true
+ elsif dns_names != node.all_dns_names
+ log :updating, "cert for node '#{node.name}' because domain name aliases have changed" do
+ log "from: #{dns_names.inspect}"
+ log "to: #{node.all_dns_names.inspect})"
+ end
+ return true
+ end
+ end
+ end
+ end
+ return false
+ end
+
+ #
+ # check the expiration of commercial certs, if any.
+ #
+ def warn_if_commercial_cert_will_soon_expire
+ require 'leap_cli/x509'
+
+ self.all_dns_names.each do |domain|
+ if Util.file_exists?([:commercial_cert, domain])
+ cert = X509.load_certificate_file([:commercial_cert, domain])
+ path = Path.relative_path([:commercial_cert, domain])
+ if cert.not_after < Time.now.utc
+ Util.log :error, "the commercial certificate '#{path}' has EXPIRED! " +
+ "You should renew it with `leap cert csr --domain #{domain}`."
+ elsif cert.not_after < Time.now.advance(:months => 2)
+ Util.log :warning, "the commercial certificate '#{path}' will expire soon (#{cert.not_after}). "+
+ "You should renew it with `leap cert csr --domain #{domain}`."
+ end
+ end
+ end
+ end
+
+ end
+
+end; end
+
diff --git a/lib/leap_cli/util/x509.rb b/lib/leap_cli/util/x509.rb
deleted file mode 100644
index 787fdfac..00000000
--- a/lib/leap_cli/util/x509.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-autoload :OpenSSL, 'openssl'
-autoload :CertificateAuthority, 'certificate_authority'
-
-require 'digest'
-require 'digest/md5'
-require 'digest/sha1'
-
-module LeapCli; module X509
- extend self
-
- #
- # returns a fingerprint of a x509 certificate
- #
- def fingerprint(digest, cert_file)
- if cert_file.is_a? String
- cert = OpenSSL::X509::Certificate.new(Util.read_file!(cert_file))
- elsif cert_file.is_a? OpenSSL::X509::Certificate
- cert = cert_file
- elsif cert_file.is_a? CertificateAuthority::Certificate
- cert = cert_file.openssl_body
- end
- digester = case digest
- when "MD5" then Digest::MD5.new
- when "SHA1" then Digest::SHA1.new
- when "SHA256" then Digest::SHA256.new
- when "SHA384" then Digest::SHA384.new
- when "SHA512" then Digest::SHA512.new
- end
- digester.hexdigest(cert.to_der)
- end
-
-
-end; end
diff --git a/lib/leap_cli/x509.rb b/lib/leap_cli/x509.rb
new file mode 100644
index 00000000..68d13ddf
--- /dev/null
+++ b/lib/leap_cli/x509.rb
@@ -0,0 +1,16 @@
+#
+# optional. load if you want access to any methods in the module X509
+#
+
+require 'date'
+require 'securerandom'
+require 'openssl'
+require 'digest'
+require 'digest/md5'
+require 'digest/sha1'
+
+require 'certificate_authority'
+
+require 'leap_cli/x509/certs'
+require 'leap_cli/x509/signing_profiles'
+require 'leap_cli/x509/utils'
diff --git a/lib/leap_cli/x509/certs.rb b/lib/leap_cli/x509/certs.rb
new file mode 100644
index 00000000..3b74d2fb
--- /dev/null
+++ b/lib/leap_cli/x509/certs.rb
@@ -0,0 +1,232 @@
+
+module LeapCli; module X509
+
+ #
+ # returns a fingerprint of a x509 certificate
+ #
+ # Note: there are different ways of computing a digest of a certificate.
+ # You can either take a digest of the entire cert in DER format, or you
+ # can take a digest of the public key.
+ #
+ # For now, we only support the DER method.
+ #
+ def self.fingerprint(digest, cert_file)
+ if cert_file.is_a? String
+ cert = OpenSSL::X509::Certificate.new(Util.read_file!(cert_file))
+ elsif cert_file.is_a? OpenSSL::X509::Certificate
+ cert = cert_file
+ elsif cert_file.is_a? CertificateAuthority::Certificate
+ cert = cert_file.openssl_body
+ end
+ digester = case digest
+ when "MD5" then Digest::MD5.new
+ when "SHA1" then Digest::SHA1.new
+ when "SHA256" then Digest::SHA256.new
+ when "SHA384" then Digest::SHA384.new
+ when "SHA512" then Digest::SHA512.new
+ end
+ digester.hexdigest(cert.to_der)
+ end
+
+ def self.ca_root
+ @ca_root ||= begin
+ load_certificate_file(:ca_cert, :ca_key)
+ end
+ end
+
+ def self.client_ca_root
+ @client_ca_root ||= begin
+ load_certificate_file(:client_ca_cert, :client_ca_key)
+ end
+ end
+
+ def self.load_certificate_file(crt_file, key_file=nil, password=nil)
+ crt = Util.read_file!(crt_file)
+ openssl_cert = OpenSSL::X509::Certificate.new(crt)
+ cert = CertificateAuthority::Certificate.from_openssl(openssl_cert)
+ if key_file
+ key = Util.read_file!(key_file)
+ cert.key_material.private_key = OpenSSL::PKey::RSA.new(key, password)
+ end
+ return cert
+ end
+
+ #
+ # creates a new certificate authority.
+ #
+ def self.new_ca(options, common_name)
+ root = CertificateAuthority::Certificate.new
+
+ # set subject
+ root.subject.common_name = common_name
+ possible = ['country', 'state', 'locality', 'organization', 'organizational_unit', 'email_address']
+ options.keys.each do |key|
+ if possible.include?(key)
+ root.subject.send(key + '=', options[key])
+ end
+ end
+
+ # set expiration
+ root.not_before = X509.yesterday
+ root.not_after = X509.yesterday_advance(options['life_span'])
+
+ # generate private key
+ root.serial_number.number = 1
+ root.key_material.generate_key(options['bit_size'])
+
+ # sign self
+ root.signing_entity = true
+ root.parent = root
+ root.sign!(ca_root_signing_profile)
+ return root
+ end
+
+ #
+ # creates a CSR in memory and returns it.
+ # with the correct extReq attribute so that the CA
+ # doens't generate certs with extensions we don't want.
+ #
+ def self.new_csr(dn, keypair, digest)
+ csr = CertificateAuthority::SigningRequest.new
+ csr.distinguished_name = dn
+ csr.key_material = keypair
+ csr.digest = digest
+
+ # define extensions manually (library doesn't support setting these on CSRs)
+ extensions = []
+ extensions << CertificateAuthority::Extensions::BasicConstraints.new.tap {|basic|
+ basic.ca = false
+ }
+ extensions << CertificateAuthority::Extensions::KeyUsage.new.tap {|keyusage|
+ keyusage.usage = ["digitalSignature", "keyEncipherment"]
+ }
+ extensions << CertificateAuthority::Extensions::ExtendedKeyUsage.new.tap {|extkeyusage|
+ extkeyusage.usage = [ "serverAuth"]
+ }
+
+ # convert extensions to attribute 'extReq'
+ # aka "Requested Extensions"
+ factory = OpenSSL::X509::ExtensionFactory.new
+ attrval = OpenSSL::ASN1::Set([OpenSSL::ASN1::Sequence(
+ extensions.map{|e| factory.create_ext(e.openssl_identifier, e.to_s, e.critical)}
+ )])
+ attrs = [
+ OpenSSL::X509::Attribute.new("extReq", attrval),
+ ]
+ csr.attributes = attrs
+
+ return csr
+ end
+
+ #
+ # creates new csr and cert files for a particular domain.
+ #
+ # The cert is signed with the ca_root, but should be replaced
+ # later with a real cert signed by a better ca
+ #
+ def self.create_csr_and_cert(options)
+ bit_size = options[:bits].to_i
+ digest = options[:digest]
+
+ # RSA key
+ keypair = CertificateAuthority::MemoryKeyMaterial.new
+ Util.log :generating, "%s bit RSA key" % bit_size do
+ keypair.generate_key(bit_size)
+ Util.write_file! [:commercial_key, options[:domain]], keypair.private_key.to_pem
+ end
+
+ # CSR
+ csr = nil
+ dn = CertificateAuthority::DistinguishedName.new
+ dn.common_name = options[:domain]
+ dn.organization = options[:organization]
+ dn.ou = options[:organizational_unit]
+ dn.email_address = options[:email]
+ dn.country = options[:country]
+ dn.state = options[:state]
+ dn.locality = options[:locality]
+ Util.log :generating, "CSR with #{digest} digest and #{print_dn(dn)}" do
+ csr = new_csr(dn, keypair, options[:digest])
+ Util.write_file! [:commercial_csr, options[:domain]], csr.to_pem
+ end
+
+ # Sign using our own CA, for use in testing but hopefully not production.
+ # It is not that commerical CAs are so secure, it is just that signing your own certs is
+ # a total drag for the user because they must click through dire warnings.
+ Util.log :generating, "self-signed x509 server certificate for testing purposes" do
+ cert = csr.to_cert
+ cert.serial_number.number = cert_serial_number(options[:domain])
+ cert.not_before = yesterday
+ cert.not_after = yesterday.advance(:years => 1)
+ cert.parent = ca_root
+ cert.sign! domain_test_signing_profile
+ Util.write_file! [:commercial_cert, options[:domain]], cert.to_pem
+ Util.log "please replace this file with the real certificate you get from a CA using #{Path.relative_path([:commercial_csr, options[:domain]])}"
+ end
+
+ # Fake CA
+ unless Util.file_exists? :commercial_ca_cert
+ Util.log :using, "generated CA in place of commercial CA for testing purposes" do
+ Util.write_file! :commercial_ca_cert, Util.read_file!(:ca_cert)
+ Util.log "please also replace this file with the CA cert from the commercial authority you use."
+ end
+ end
+ end
+
+ #
+ # Return true if the given server cert has been signed by the given CA cert
+ #
+ # This does not actually validate the signature, it just checks the cert
+ # extensions.
+ #
+ def self.created_by_authority?(cert, ca=X509.ca_root)
+ authority_key_id = cert.extensions["authorityKeyIdentifier"].identifier.sub(/^keyid:/, '')
+ return authority_key_id == self.public_key_id_for_ca(ca)
+ 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 self.cert_serial_number(domain_name)
+ Digest::MD5.hexdigest("#{domain_name} -- #{Time.now}").to_i(16)
+ end
+
+ #
+ # for the random common name, we need a text string that will be
+ # unique across all certs.
+ #
+ def self.random_common_name(domain_name)
+ #cert_serial_number(domain_name).to_s(36)
+ SecureRandom.uuid
+ end
+
+ private
+
+ #
+ # calculate the "key id" for a root CA, that matches the value
+ # Authority Key Identifier in the x509 extensions of a cert.
+ #
+ def self.public_key_id_for_ca(ca_cert)
+ @ca_key_ids ||= {}
+ @ca_key_ids[ca_cert.object_id] ||= begin
+ pubkey = ca_cert.key_material.public_key
+ seq = OpenSSL::ASN1::Sequence([
+ OpenSSL::ASN1::Integer.new(pubkey.n),
+ OpenSSL::ASN1::Integer.new(pubkey.e)
+ ])
+ Digest::SHA1.hexdigest(seq.to_der).upcase.scan(/../).join(':')
+ end
+ end
+
+ # prints CertificateAuthority::DistinguishedName fields
+ def self.print_dn(dn)
+ fields = {}
+ [:common_name, :locality, :state, :country, :organization, :organizational_unit, :email_address].each do |attr|
+ fields[attr] = dn.send(attr) if dn.send(attr)
+ end
+ fields.inspect
+ end
+
+end; end
diff --git a/lib/leap_cli/x509/signing_profiles.rb b/lib/leap_cli/x509/signing_profiles.rb
new file mode 100644
index 00000000..56cd29c7
--- /dev/null
+++ b/lib/leap_cli/x509/signing_profiles.rb
@@ -0,0 +1,104 @@
+#
+# Signing profiles are used by CertificateAuthority in order to
+# set the correct flags when signing certificates.
+#
+
+module LeapCli; module X509
+
+ #
+ # For CA self-signing
+ #
+ def self.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.
+ # Web browsers seem to break without keyEncipherment.
+ # For now, I am using digitalSignature + keyEncipherment
+ #
+ # * digitalSignature -- for (EC)DHE cipher suites
+ # "The digitalSignature bit is asserted when the subject public key is used
+ # with a digital signature mechanism to support security services other
+ # than certificate signing (bit 5), or CRL signing (bit 6). Digital
+ # signature mechanisms are often used for entity authentication and data
+ # origin authentication with integrity."
+ #
+ # * keyEncipherment ==> for plain RSA cipher suites
+ # "The keyEncipherment bit is asserted when the subject public key is used for
+ # key transport. For example, when an RSA key is to be used for key management,
+ # then this bit is set."
+ #
+ # * keyAgreement ==> for used with DH, not RSA.
+ # "The keyAgreement bit is asserted when the subject public key is used for key
+ # agreement. For example, when a Diffie-Hellman key is to be used for key
+ # management, then this bit is set."
+ #
+ # digest options: SHA512, SHA256, SHA1
+ #
+ def self.server_signing_profile(node)
+ {
+ "digest" => node.env.provider.ca.server_certificates.digest,
+ "extensions" => {
+ "keyUsage" => {
+ "usage" => ["digitalSignature", "keyEncipherment"]
+ },
+ "extendedKeyUsage" => {
+ "usage" => ["serverAuth", "clientAuth"]
+ },
+ "subjectAltName" => {
+ "ips" => [node.ip_address],
+ "dns_names" => node.all_dns_names
+ }
+ }
+ }
+ end
+
+ #
+ # This is used when signing the main cert for the provider's domain
+ # with our own CA (for testing purposes). Typically, this cert would
+ # be purchased from a commercial CA, and not signed this way.
+ #
+ def self.domain_test_signing_profile
+ {
+ "digest" => "SHA256",
+ "extensions" => {
+ "keyUsage" => {
+ "usage" => ["digitalSignature", "keyEncipherment"]
+ },
+ "extendedKeyUsage" => {
+ "usage" => ["serverAuth"]
+ }
+ }
+ }
+ end
+
+ #
+ # This is used when signing a dummy client certificate that is only to be
+ # used for testing.
+ #
+ def self.client_test_signing_profile
+ {
+ "digest" => "SHA256",
+ "extensions" => {
+ "keyUsage" => {
+ "usage" => ["digitalSignature"]
+ },
+ "extendedKeyUsage" => {
+ "usage" => ["clientAuth"]
+ }
+ }
+ }
+ end
+
+end; end \ No newline at end of file
diff --git a/lib/leap_cli/x509/utils.rb b/lib/leap_cli/x509/utils.rb
new file mode 100644
index 00000000..98ff9c0b
--- /dev/null
+++ b/lib/leap_cli/x509/utils.rb
@@ -0,0 +1,26 @@
+module LeapCli; module X509
+
+ #
+ # TIME HELPERS
+ #
+ # note: we use 'yesterday' instead of 'today', because times are in UTC, and
+ # some people on the planet are behind UTC!
+ #
+
+ def self.yesterday
+ t = Time.now - 24*24*60
+ Time.utc t.year, t.month, t.day
+ end
+
+ def self.yesterday_advance(string)
+ number, unit = string.split(' ')
+ unless ['years', 'months', 'days', 'hours', 'minutes'].include? unit
+ bail!("The time property '#{string}' is missing a unit (one of: years, months, days, hours, minutes).")
+ end
+ unless number.to_i.to_s == number
+ bail!("The time property '#{string}' is missing a number.")
+ end
+ yesterday.advance(unit.to_sym => number.to_i)
+ end
+
+end; end \ No newline at end of file