Skip to content

Instantly share code, notes, and snippets.

@QNENet
Created November 26, 2009 06:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save QNENet/243275 to your computer and use it in GitHub Desktop.
Save QNENet/243275 to your computer and use it in GitHub Desktop.
require 'rubygems'
require 'openssl'
##
# :main: README.txt
#
# 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]}[http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/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 = `hostname`.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.
class QuickCert
##
# QuickCert Version
VERSION = "2.0"
##
# Creates a new QuickCert instance using the Certificate Authority described
# in +ca_config+. If there is no CA at ca_config[:CA_dir], then QuickCert
# will initialize a new one. Prints out debugging info if +debug+ is true.
def initialize(ca_config, debug = false)
@ca_config = ca_config
@debug = debug
create_ca
end
##
# Creates a new certificate from +cert_config+ that is signed
# by the CA.
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
##
# Creates a new Certificate Authority from @ca_config if it
# does not already exist at ca_config[:CA_dir].
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 = 10001
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
##
# Creates a new RSA key from +cert_config+.
def create_key(cert_config)
dest = cert_config[:hostname] || cert_config[:user]
keypair_file = File.join dest, (dest + "_keypair.pem")
Dir.mkdir dest, 0700 unless File.exists? dest
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
##
# Creates a new Certificate Signing Request for the keypair in
# +keypair_file+, generating and saving new keypair if nil.
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
##
# Signs the certificate described in +cert_config+ and
# +csr_file+, saving it to +cert_file+.
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
end
CA = {}
CERTS = []
CA[:hostname] = 'host'
CA[:domainname] = 'host.example.com'
CA[:CA_dir] = File.join Dir.pwd, "CA"
CA[:password] = '1234'
CA[:CA_dir] ||= Dir.pwd
CA[:keypair_file] ||= File.join CA[:CA_dir], "private/cakeypair.pem"
CA[:cert_file] ||= File.join CA[:CA_dir], "cacert.pem"
CA[:serial_file] ||= File.join CA[:CA_dir], "serial"
CA[:new_certs_dir] ||= File.join CA[:CA_dir], "newcerts"
CA[:new_keypair_dir] ||= File.join CA[:CA_dir], "private/keypair_backup"
CA[:crl_dir] ||= File.join CA[:CA_dir], "crl"
CA[:ca_cert_days] ||= 5 * 365 # five years
CA[:ca_rsa_key_length] ||= 2048
CA[:cert_days] ||= 365 # one year
CA[:cert_key_length_min] ||= 1024
CA[:cert_key_length_max] ||= 2048
CA[:crl_file] ||= File.join CA[:crl_dir], "#{CA[:hostname]}.crl"
CA[:crl_pem_file] ||= File.join CA[:crl_dir], "#{CA[:hostname]}.pem"
CA[:crl_days] ||= 14
if CA[:name].nil?
CA[:name] = [
['C', 'US', OpenSSL::ASN1::PRINTABLESTRING],
['O', CA[:domainname], OpenSSL::ASN1::UTF8STRING],
['OU', CA[:hostname], OpenSSL::ASN1::UTF8STRING],
]
end
CERTS << {
:type => 'server',
:hostname => 'host',
:password => '5678',
}
CERTS << {
:type => 'client',
:user => 'user',
:email => 'user@example.com',
}
qc = QuickCert.new(CA, true)
CERTS.each do |new_certificate_opts|
qc.create_cert(new_certificate_opts)
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment