diff options
| author | Micah Anderson <micah@riseup.net> | 2016-11-04 10:54:28 -0400 | 
|---|---|---|
| committer | Micah Anderson <micah@riseup.net> | 2016-11-04 10:54:28 -0400 | 
| commit | 34a381efa8f6295080c843f86bfa07d4e41056af (patch) | |
| tree | 9282cf5d4c876688602705a7fa0002bc4a810bde /lib/leap_cli/x509 | |
| parent | 0a72bc6fd292bf9367b314fcb0347c4d35042f16 (diff) | |
| parent | 5821964ff7e16ca7aa9141bd09a77d355db492a9 (diff) | |
Merge branch 'develop'
Diffstat (limited to 'lib/leap_cli/x509')
| -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 | 
3 files changed, 362 insertions, 0 deletions
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  | 
