summaryrefslogtreecommitdiff
path: root/lib/leap_cli/commands/ca.rb
diff options
context:
space:
mode:
authorelijah <elijah@riseup.net>2016-12-21 11:16:24 -0800
committerelijah <elijah@riseup.net>2016-12-21 11:16:24 -0800
commit8ab553dfcaa1aff10123d15c908117b59d2d5b7d (patch)
tree4336d40f45b6f8e8d59a3fd861bbc9fadc35fd16 /lib/leap_cli/commands/ca.rb
parent4116559c43b36cf69dc128769cbae857c53f094f (diff)
rename s/ca.rb/cert.rb/
Diffstat (limited to 'lib/leap_cli/commands/ca.rb')
-rw-r--r--lib/leap_cli/commands/ca.rb368
1 files changed, 0 insertions, 368 deletions
diff --git a/lib/leap_cli/commands/ca.rb b/lib/leap_cli/commands/ca.rb
deleted file mode 100644
index 1c67ae67..00000000
--- a/lib/leap_cli/commands/ca.rb
+++ /dev/null
@@ -1,368 +0,0 @@
-module LeapCli; module Commands
-
- desc "Manage X.509 certificates"
- command :cert do |cert|
-
- cert.desc 'Creates two Certificate Authorities (one for validating servers and one for validating clients).'
- 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>`.'
- cert.command :ca do |ca|
- ca.action do |global_options,options,args|
- assert_config! 'provider.ca.name'
- generate_new_certificate_authority(:ca_key, :ca_cert, provider.ca.name)
- generate_new_certificate_authority(:client_ca_key, :client_ca_cert, provider.ca.name + ' (client certificates only!)')
- end
- end
-
- cert.desc 'Creates or renews a X.509 certificate/key pair for a single node or all nodes, but only if needed.'
- cert.long_desc 'This command will a generate new certificate for a node if some value in the node has changed ' +
- 'that is included in the certificate (like hostname or IP address), or if the old certificate will be expiring soon. ' +
- 'Sometimes, you might want to force the generation of a new certificate, ' +
- 'such as in the cases where you have changed a CA parameter for server certificates, like bit size or digest hash. ' +
- 'In this case, use --force. If <node-filter> is empty, this command will apply to all nodes.'
- cert.arg_name 'FILTER'
- cert.command :update do |update|
- update.switch 'force', :desc => 'Always generate new certificates', :negatable => false
- update.action do |global_options,options,args|
- update_certificates(manager.filter!(args), options)
- end
- end
-
- 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|
- generate_dh
- end
- end
-
- 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`, "+
- "but may be overridden here."
- cert.arg_name "DOMAIN"
- cert.command :csr do |csr|
- csr.flag 'domain', :arg_name => 'DOMAIN', :desc => 'Specify what domain to create the CSR for.'
- csr.flag ['organization', 'O'], :arg_name => 'ORGANIZATION', :desc => "Override default O in distinguished name."
- csr.flag ['unit', 'OU'], :arg_name => 'UNIT', :desc => "Set OU in distinguished name."
- csr.flag 'email', :arg_name => 'EMAIL', :desc => "Set emailAddress in distinguished name."
- csr.flag ['locality', 'L'], :arg_name => 'LOCALITY', :desc => "Set L in distinguished name."
- csr.flag ['state', 'ST'], :arg_name => 'STATE', :desc => "Set ST in distinguished name."
- csr.flag ['country', 'C'], :arg_name => 'COUNTRY', :desc => "Set C in distinguished name."
- 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|
- generate_csr(global_options, options, args)
- end
- end
-
- cert.desc "Register an authorization key with the CA letsencrypt.org"
- cert.long_desc "This only needs to be done once."
- cert.command :register do |register|
- register.action do |global, options, args|
- do_register_key(global, options, args)
- end
- end
-
- cert.desc "Renews a certificate using the CA letsencrypt.org"
- cert.arg_name "DOMAIN"
- cert.command :renew do |renew|
- renew.action do |global, options, args|
- do_renew_cert(global, options, args)
- end
- end
-
- end
-
- protected
-
- #
- # 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'
- assert_config! 'provider.ca.server_certificates.life_span'
- assert_config! 'common.x509.use'
-
- nodes.each_node do |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] || node.cert_needs_updating?
- node.generate_cert
- end
- end
- 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 = 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
-
- private
-
- 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'
-
- root = X509.new_ca(provider.ca, common_name)
-
- write_file!(key_file, root.key_material.private_key.to_pem)
- write_file!(cert_file, root.to_pem)
- end
-
- 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
- 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
- #
- 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'
-
- server_certificates = provider.ca.server_certificates
- options[:domain] ||= args.first || 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
-
- 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
-
- X509.create_csr_and_cert(options)
- end
-
- #
- # letsencrypt.org
- #
-
- def do_register_key(global, options, args)
- require 'leap_cli/acme'
- assert_config! 'provider.contacts.default'
- contact = manager.provider.contacts.default.first
-
- if file_exists?(:acme_key) && !global[:force]
- bail! do
- log "the authorization key for letsencrypt.org already exists"
- log "run with --force if you really want to register a new key."
- end
- else
- private_key = Acme.new_private_key
- registration = nil
-
- log(:registering, "letsencrypt.org authorization key using contact `%s`" % contact) do
- acme = Acme.new(key: private_key)
- registration = acme.register(contact)
- if registration
- log 'success!', :color => :green, :style => :bold
- else
- bail! "could not register authorization key."
- end
- end
-
- log :saving, "authorization key for letsencrypt.org" do
- write_file!(:acme_key, private_key.to_pem)
- write_file!(:acme_info, JSON.sorted_generate({
- id: registration.id,
- contact: registration.contact,
- key: registration.key,
- uri: registration.uri
- }))
- log :warning, "keep key file private!"
- end
- end
- end
-
- def assert_no_errors!(msg)
- yield
- rescue StandardError => exc
- bail! :error, msg do
- log exc.to_s
- end
- end
-
- def do_renew_cert(global, options, args)
- require 'leap_cli/acme'
- require 'leap_cli/ssh'
- require 'socket'
- require 'net/http'
-
- csr = nil
- account_key = nil
- cert = nil
- acme = nil
-
- #
- # sanity check the domain
- #
- domain = args.first
- nodes = nodes_for_domain(domain)
- domain_ready_for_acme!(domain)
-
- #
- # load key material
- #
- assert_files_exist!([:commercial_key, domain], [:commercial_csr, domain],
- :msg => 'Please create the CSR first with `leap cert csr %s`' % domain)
- assert_no_errors!("Could not load #{path([:commercial_csr, domain])}") do
- csr = Acme.load_csr(read_file!([:commercial_csr, domain]))
- end
- assert_files_exist!(:acme_key,
- :msg => "Please run `leap cert register` first. This only needs to be done once.")
- assert_no_errors!("Could not load #{path(:acme_key)}") do
- account_key = Acme.load_private_key(read_file!(:acme_key))
- end
-
- #
- # check authorization for this domain
- #
- log :checking, "authorization"
- acme = Acme.new(domain: domain, key: account_key)
- status, message = acme.authorize do |challenge|
- log(:uploading, 'challenge to server %s' % domain) do
- SSH.remote_command(nodes) do |ssh, host|
- ssh.scripts.upload_acme_challenge(challenge.token, challenge.file_content)
- end
- end
- log :waiting, "for letsencrypt.org to verify challenge"
- end
- if status == 'valid'
- log 'authorized!', color: :green, style: :bold
- elsif status == 'error'
- bail! :error, message.inspect
- elsif status == 'unauthorized'
- bail!(:unauthorized, message.inspect, color: :yellow, style: :bold) do
- log 'You must first run `leap cert register` to register the account key with letsencrypt.org'
- end
- else
- bail!(:error, "unrecognized status: #{status.inspect}, #{message.inspect}")
- end
-
- log :fetching, "new certificate from letsencrypt.org"
- assert_no_errors!("could not renew certificate") do
- cert = acme.get_certificate(csr)
- end
- log 'success', color: :green, style: :bold
- write_file!([:commercial_cert, domain], cert.fullchain_to_pem)
- log 'You should now run `leap deploy` to deploy the new certificate.'
- end
-
- #
- # Returns a hash of nodes that match this domain. It also checks:
- #
- # * a node configuration has this domain
- # * the dns for the domain exists
- #
- # This method will bail if any checks fail.
- #
- def nodes_for_domain(domain)
- bail! { log 'Argument DOMAIN is required' } if domain.nil? || domain.empty?
- nodes = manager.nodes['dns.aliases' => domain]
- if nodes.empty?
- bail! :error, "There are no nodes configured for domain `%s`" % domain
- end
- begin
- ips = Socket.getaddrinfo(domain, 'http').map {|record| record[2]}.uniq
- nodes = nodes['ip_address' => ips]
- if nodes.empty?
- bail! do
- log :error, "The domain `%s` resolves to [%s]" % [domain, ips.join(', ')]
- log :error, "But there no nodes configured for this domain with these adddresses."
- end
- end
- rescue SocketError
- bail! :error, "Could not resolve the DNS for `#{domain}`. Without a DNS " +
- "entry for this domain, authorization will not work."
- end
- return nodes
- end
-
- #
- # runs the following checks on the domain:
- #
- # * we are able to get /.well-known/acme-challenge/ok
- #
- # This method will bail if any checks fail.
- #
- def domain_ready_for_acme!(domain)
- begin
- uri = URI("https://#{domain}/.well-known/acme-challenge/ok")
- options = {
- use_ssl: true,
- open_timeout: 5,
- verify_mode: OpenSSL::SSL::VERIFY_NONE
- }
- Net::HTTP.start(uri.host, uri.port, options) do |http|
- http.request(Net::HTTP::Get.new(uri)) do |response|
- if !response.is_a?(Net::HTTPSuccess)
- bail!(:error, "Could not GET %s" % uri) do
- log "%s %s" % [response.code, response.message]
- log "You may need to run `leap deploy`"
- end
- end
- end
- end
- rescue Errno::ETIMEDOUT, Net::OpenTimeout
- bail! :error, "Connection attempt timed out: %s" % uri
- rescue Interrupt
- bail!
- rescue StandardError => exc
- bail!(:error, "Could not GET %s" % uri) do
- log exc.to_s
- end
- end
- end
-
-end; end