Created
November 26, 2009 06:13
-
-
Save QNENet/243275 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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