diff options
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/leap_cli/commands/ca.rb | 493 | ||||
| -rw-r--r-- | lib/leap_cli/commands/node.rb | 157 | ||||
| -rw-r--r-- | lib/leap_cli/config/node.rb | 176 | ||||
| -rw-r--r-- | lib/leap_cli/config/node_cert.rb | 124 | ||||
| -rw-r--r-- | lib/leap_cli/util/x509.rb | 33 | ||||
| -rw-r--r-- | lib/leap_cli/x509.rb | 16 | ||||
| -rw-r--r-- | lib/leap_cli/x509/certs.rb | 232 | ||||
| -rw-r--r-- | lib/leap_cli/x509/signing_profiles.rb | 104 | ||||
| -rw-r--r-- | lib/leap_cli/x509/utils.rb | 26 | 
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 | 
