class QuickCert

QuickCert allows you to quickly and easily create SSL certificates. It uses a simple configuration file to generate self-signed client and server certificates.

QuickCert is a compilation of NAKAMURA Hiroshi's post [ruby-talk:89917]

the example scripts referenced in the above post, and gen_csr.rb from Ruby’s OpenSSL examples.

A simple QuickCert configuration file looks like:

full_hostname = %xhostname`.strip
domainname = full_hostname.split('.')[1..-1].join('.')
hostname = full_hostname.split('.')[0]

CA[:hostname] = hostname
CA[:domainname] = domainname
CA[:CA_dir] = File.join Dir.pwd, "CA"
CA[:password] = '1234'

CERTS << {
  :type => 'server',
  :hostname => 'uriel',
  :password => '5678',
}

CERTS << {
  :type => 'client',
  :user => 'drbrain',
  :email => 'drbrain@segment7.net',
}

This configuration will create a Certificate Authority in a ‘CA’ directory in the current directory, a server certificate with password ‘5678’ for the server ‘uriel’ in a directory named ‘uriel’, and a client certificate for drbrain in the directory ‘drbrain’ with no password.

There are additional SSL knobs you can tweak in the qc_defaults.rb file. (See `gem which quick_cert/defaults`).

To generate the certificates, simply create a qc_config file where you want the certificate directories to be created, then run QuickCert.

Constants

VERSION

QuickCert Version

Public Class Methods

new(ca_config, debug = false) click to toggle source

Creates a new QuickCert instance using the Certificate Authority described in ca_config. If there is no CA at ca_config, then QuickCert will initialize a new one. Prints out debugging info if debug is true.

# File lib/quick_cert.rb, line 62
def initialize(ca_config, debug = false)
  @ca_config = ca_config
  @debug = debug

  create_ca
end

Public Instance Methods

create_ca() click to toggle source

Creates a new Certificate Authority from @ca_config if it does not already exist at ca_config.

# File lib/quick_cert.rb, line 83
def create_ca
  return if File.exist? @ca_config[:CA_dir]

  Dir.mkdir @ca_config[:CA_dir]

  Dir.mkdir File.join(@ca_config[:CA_dir], 'private'), 0700
  Dir.mkdir File.join(@ca_config[:CA_dir], 'newcerts')
  Dir.mkdir File.join(@ca_config[:CA_dir], 'crl')

  open @ca_config[:serial_file], 'w' do |f| f << '1' end

  warn "Generating CA keypair" if @debug
  keypair = OpenSSL::PKey::RSA.new @ca_config[:ca_rsa_key_length]

  cert = OpenSSL::X509::Certificate.new
  name = @ca_config[:name].dup << ['CN', 'CA']
  cert.subject = cert.issuer = OpenSSL::X509::Name.new(name)
  cert.not_before = Time.now
  cert.not_after = Time.now + @ca_config[:ca_cert_days] * 24 * 60 * 60
  cert.public_key = keypair.public_key
  cert.serial = 0x0
  cert.version = 2 # X509v3

  ef = OpenSSL::X509::ExtensionFactory.new
  ef.subject_certificate = cert
  ef.issuer_certificate = cert
  cert.extensions = [
    ef.create_extension("basicConstraints", "CA:TRUE", true),
    ef.create_extension("nsComment", "Ruby/OpenSSL Generated Certificate"),
    ef.create_extension("subjectKeyIdentifier", "hash"),
    ef.create_extension("keyUsage", "cRLSign,keyCertSign", true),
  ]
  cert.add_extension ef.create_extension("authorityKeyIdentifier",
                                         "keyid:always,issuer:always")
  cert.sign keypair, OpenSSL::Digest::SHA1.new

  keypair_export = keypair.export OpenSSL::Cipher::DES.new(:EDE3, :CBC),
                                  @ca_config[:password]

  warn "Writing keypair to #{@ca_config[:keypair_file]}" if @debug
  open @ca_config[:keypair_file], "w", 0400 do |fp|
    fp << keypair_export
  end

  warn "Writing cert to #{@ca_config[:cert_file]}" if @debug
  open @ca_config[:cert_file], "w", 0644 do |f|
    f << cert.to_pem
  end

  warn "Done generating certificate for #{cert.subject}" if @debug
end
create_cert(cert_config) click to toggle source

Creates a new certificate from cert_config that is signed by the CA.

# File lib/quick_cert.rb, line 73
def create_cert(cert_config)
  cert_keypair = create_key cert_config
  cert_csr = create_csr cert_config, cert_keypair
  sign_cert cert_config, cert_keypair, cert_csr
end
create_csr(cert_config, keypair_file = nil) click to toggle source

Creates a new Certificate Signing Request for the keypair in keypair_file, generating and saving new keypair if nil.

# File lib/quick_cert.rb, line 168
def create_csr(cert_config, keypair_file = nil)
  keypair = nil
  dest = cert_config[:hostname] || cert_config[:user]
  csr_file = File.join dest, "csr_#{dest}.pem"

  name = @ca_config[:name].dup

  case cert_config[:type]
  when 'server' then
    name << ['OU', 'CA']
    name << ['CN', cert_config[:hostname]]
  when 'client' then
    name << ['CN', cert_config[:user]]
    name << ['emailAddress', cert_config[:email]]
  end

  name = OpenSSL::X509::Name.new name

  if File.exist? keypair_file then
    keypair = OpenSSL::PKey::RSA.new File.read(keypair_file),
                                     cert_config[:password]
  else
    keypair = create_key cert_config
  end

  warn "Generating CSR for #{name}" if @debug

  req = OpenSSL::X509::Request.new
  req.version = 0
  req.subject = name
  req.public_key = keypair.public_key
  req.sign keypair, OpenSSL::Digest::MD5.new

  warn "Writing CSR to #{csr_file}" if @debug
  open csr_file, "w" do |f|
    f << req.to_pem
  end

  csr_file
end
create_key(cert_config) click to toggle source

Creates a new RSA key from cert_config.

# File lib/quick_cert.rb, line 138
def create_key(cert_config)
  dest = cert_config[:hostname] || cert_config[:user]
  keypair_file = File.join dest, (dest + "_keypair.pem")
  Dir.mkdir dest, 0700

  warn "Generating RSA keypair" if @debug
  keypair = OpenSSL::PKey::RSA.new 1024

  if cert_config[:password].nil? then
    open keypair_file, "w", 0400 do |f|
      f << keypair.to_pem
    end
  else
    keypair_export = keypair.export OpenSSL::Cipher::DES.new(:EDE3, :CBC),
                                    cert_config[:password]

    warn "Writing keypair to #{keypair_file}" if @debug
    open keypair_file, "w", 0400 do |f|
      f << keypair_export
    end

  end

  keypair_file
end
sign_cert(cert_config, cert_file, csr_file) click to toggle source

Signs the certificate described in cert_config and csr_file, saving it to cert_file.

# File lib/quick_cert.rb, line 213
def sign_cert(cert_config, cert_file, csr_file)
  csr = OpenSSL::X509::Request.new File.read(csr_file)

  raise "CSR sign verification failed." unless csr.verify csr.public_key

  raise "Key length too short" if
    csr.public_key.n.num_bits < @ca_config[:cert_key_length_min]

  raise "Key length too long" if
    csr.public_key.n.num_bits > @ca_config[:cert_key_length_max]

  raise "DN does not match" if
    csr.subject.to_a[0, @ca_config[:name].size] != @ca_config[:name]

  # Only checks signature here.  You must verify CSR according to your
  # CP/CPS.

  # CA setup

  warn "Reading CA cert from #{@ca_config[:cert_file]}" if @debug
  ca = OpenSSL::X509::Certificate.new File.read(@ca_config[:cert_file])

  warn "Reading CA keypair from #{@ca_config[:keypair_file]}" if @debug
  ca_keypair = OpenSSL::PKey::RSA.new File.read(@ca_config[:keypair_file]),
                                      @ca_config[:password]

  serial = File.read(@ca_config[:serial_file]).chomp.hex
  open @ca_config[:serial_file], "w" do |f|
    f << "%04X" % (serial + 1)
  end

  warn "Generating cert" if @debug

  cert = OpenSSL::X509::Certificate.new
  from = Time.now
  cert.subject = csr.subject
  cert.issuer = ca.subject
  cert.not_before = from
  cert.not_after = from + @ca_config[:cert_days] * 24 * 60 * 60
  cert.public_key = csr.public_key
  cert.serial = serial
  cert.version = 2 # X509v3

  basic_constraint = nil
  key_usage = []
  ext_key_usage = []

  case cert_config[:type]
  when "ca" then
    basic_constraint = "CA:TRUE"
    key_usage << "cRLSign" << "keyCertSign"
  when "terminalsubca" then
    basic_constraint = "CA:TRUE,pathlen:0"
    key_usage << "cRLSign" << "keyCertSign"
  when "server" then
    basic_constraint = "CA:FALSE"
    key_usage << "digitalSignature" << "keyEncipherment"
    ext_key_usage << "serverAuth"
  when "ocsp" then
    basic_constraint = "CA:FALSE"
    key_usage << "nonRepudiation" << "digitalSignature"
    ext_key_usage << "serverAuth" << "OCSPSigning"
  when "client" then
    basic_constraint = "CA:FALSE"
    key_usage << "nonRepudiation" << "digitalSignature" << "keyEncipherment"
    ext_key_usage << "clientAuth" << "emailProtection"
  else
    raise "unknonw cert type \"#{cert_config[:type]}\""
  end

  ef = OpenSSL::X509::ExtensionFactory.new
  ef.subject_certificate = cert
  ef.issuer_certificate = ca
  ex = []
  ex << ef.create_extension("basicConstraints", basic_constraint, true)
  ex << ef.create_extension("nsComment",
                            "Ruby/OpenSSL Generated Certificate")
  ex << ef.create_extension("subjectKeyIdentifier", "hash")
  #ex << ef.create_extension("nsCertType", "client, email")
  unless key_usage.empty? then
    ex << ef.create_extension("keyUsage", key_usage.join(","))
  end
  #ex << ef.create_extension("authorityKeyIdentifier",
  #                          "keyid:always,issuer:always")
  #ex << ef.create_extension("authorityKeyIdentifier", "keyid:always")
  unless ext_key_usage.empty? then
    ex << ef.create_extension("extendedKeyUsage", ext_key_usage.join(","))
  end

  if @ca_config[:cdp_location] then
    ex << ef.create_extension("crlDistributionPoints",
                              @ca_config[:cdp_location])
  end

  if @ca_config[:ocsp_location] then
    ex << ef.create_extension("authorityInfoAccess",
                              "OCSP;" << @ca_config[:ocsp_location])
  end
  cert.extensions = ex
  cert.sign ca_keypair, OpenSSL::Digest::SHA1.new

  backup_cert_file = @ca_config[:new_certs_dir] + "/cert_#{cert.serial}.pem"
  warn "Writing backup cert to #{backup_cert_file}" if @debug
  open backup_cert_file, "w", 0644 do |f|
    f << cert.to_pem
  end

  # Write cert
  dest = cert_config[:hostname] || cert_config[:user]
  cert_file = File.join dest, "cert_#{dest}.pem"
  warn "Writing cert to #{cert_file}" if @debug
  open cert_file, "w", 0644 do |f|
    f << cert.to_pem
  end

  cert_file
end