From c37a35df81b2d6becc09f1820240db24c3ec632c Mon Sep 17 00:00:00 2001 From: elijah Date: Mon, 12 Nov 2012 23:53:51 -0800 Subject: first fully working version of leap_ca --- lib/leap_ca.rb | 22 ++++++++ lib/leap_ca/cert.rb | 131 ++++++++++++++++++++++++++++++++++--------- lib/leap_ca/config.rb | 71 +++++++++++++++++++++++ lib/leap_ca/couch_changes.rb | 26 +++++---- lib/leap_ca/couch_stream.rb | 36 ++++++------ lib/leap_ca/pool.rb | 9 +-- lib/leap_ca/version.rb | 3 +- lib/leap_ca_daemon.rb | 24 ++++++++ 8 files changed, 262 insertions(+), 60 deletions(-) create mode 100644 lib/leap_ca/config.rb create mode 100644 lib/leap_ca_daemon.rb (limited to 'lib') diff --git a/lib/leap_ca.rb b/lib/leap_ca.rb index 9720c81..aca2925 100644 --- a/lib/leap_ca.rb +++ b/lib/leap_ca.rb @@ -1,3 +1,25 @@ +unless defined? BASE_DIR + BASE_DIR = File.expand_path('../..', __FILE__) +end +unless defined? LEAP_CA_CONFIG + LEAP_CA_CONFIG = '/etc/leap/leap_ca.yaml' +end + +# +# Load Config +# this must come first, because CouchRest needs the connection defined before the models are defined. +# +require 'leap_ca/config' +LeapCA::Config.load(BASE_DIR, 'config/config_default.yaml', LEAP_CA_CONFIG, ARGV.grep(/\.ya?ml$/).first) + +require 'couchrest_model' +CouchRest::Model::Base.configure do |config| + config.connection = LeapCA::Config.couch_connection +end + +# +# Load LeapCA +# require 'leap_ca/cert' require 'leap_ca/couch_stream' require 'leap_ca/couch_changes' diff --git a/lib/leap_ca/cert.rb b/lib/leap_ca/cert.rb index 1459e01..9e4d8ef 100644 --- a/lib/leap_ca/cert.rb +++ b/lib/leap_ca/cert.rb @@ -1,42 +1,119 @@ -require 'couchrest_model' +# +# Model for certificates stored in CouchDB. +# +# This file must be loaded after Config has been loaded. +# -class Cert < CouchRest::Model::Base +require 'base64' +require 'digest/md5' +require 'openssl' +require 'certificate_authority' +require 'date' - use_database 'certs' +module LeapCA + class Cert < CouchRest::Model::Base - timestamps! + use_database LeapCA::Config.db_name - property :random, Float, :accessible => false + timestamps! - before_validation :set_random, :attach_zip, :on => :create + property :key, String # the client private RSA key + property :cert, String # the client x509 certificate, signed by the CA + property :valid_until, Time # expiration time of the client certificate + property :random, Float, :accessible => false # used to help pick a random cert by the webapp - validates :random, :presence => true, - :numericality => {:greater_than => 0, :less_than => 1} + validates :key, :presence => true + validates :cert, :presence => true + validates :random, :presence => true, :numericality => {:greater_than_or_equal_to => 0, :less_than => 1} - validates :zip_attachment, :presence => true + before_validation :generate, :set_random, :on => :create - design do - end + design do + end - def set_random - self.random = rand - end + # + # generate the private key and client certificate + # + def generate + cert = CertificateAuthority::Certificate.new - def attach_zip - file = File.open File.join(LEAP_CA_ROOT, "config", "cert") - self.create_attachment :file => file, :name => zipname - end + # set subject + cert.subject.common_name = random_common_name - def zipname - 'cert.txt' - end + # set expiration + self.valid_until = months_from_today(Config.client_cert_lifespan) + cert.not_before = today + cert.not_after = self.valid_until - def zip_attachment - attachments[zipname] - end + # generate key + cert.serial_number.number = cert_serial_number + cert.key_material.generate_key(Config.client_cert_bit_size) - def zipped - read_attachment(zipname) - end + # sign + cert.parent = Cert.root_ca + cert.sign! client_signing_profile + + self.key = cert.key_material.private_key.to_pem + self.cert = cert.to_pem + end + + private + def set_random + self.random = rand + end + + def self.root_ca + @root_ca ||= begin + crt = File.read(Config.ca_cert_path) + key = File.read(Config.ca_key_path) + openssl_cert = OpenSSL::X509::Certificate.new(crt) + cert = CertificateAuthority::Certificate.from_openssl(openssl_cert) + cert.key_material.private_key = OpenSSL::PKey::RSA.new(key, Config.ca_key_password) + cert + end + 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 cert_serial_number + Digest::MD5.hexdigest("#{rand(10**10)} -- #{Time.now}").to_i(16) + end + + # + # 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 + cert_serial_number.to_s(36) + end + + def today + t = Time.now + Time.utc t.year, t.month, t.day + end + + def months_from_today(num) + date = Date.today >> num # >> is months in the future operator + Time.utc date.year, date.month, date.day + end + + def client_signing_profile + { + "digest" => Config.client_cert_hash, + "extensions" => { + "keyUsage" => { + "usage" => ["digitalSignature", "keyAgreement"] + }, + "extendedKeyUsage" => { + "usage" => ["clientAuth"] + } + } + } + end + + end end diff --git a/lib/leap_ca/config.rb b/lib/leap_ca/config.rb new file mode 100644 index 0000000..79616c2 --- /dev/null +++ b/lib/leap_ca/config.rb @@ -0,0 +1,71 @@ +require 'yaml' + +module LeapCA + module Config + extend self + + attr_accessor :ca_key_path + attr_accessor :ca_key_password + attr_accessor :ca_cert_path + + attr_accessor :max_pool_size + attr_accessor :client_cert_lifespan + attr_accessor :client_cert_bit_size + attr_accessor :client_cert_hash + + attr_accessor :db_name + attr_accessor :couch_connection + + def self.load(base_dir, *configs) + configs.each do |file_path| + file_path = find_file(base_dir, file_path) + next unless file_path + puts " * Loading configuration #{file_path}" + yml = YAML.load(File.read(file_path)) + if yml + yml.each do |key, value| + begin + if value.is_a? Hash + value = symbolize_keys(value) + end + self.send("#{key}=", value) + rescue NoMethodError => exc + STDERR.puts "ERROR in file #{file}, '#{key}' is not a valid option" + exit(1) + end + end + end + end + [:ca_key_path, :ca_cert_path].each do |attr| + path = self.send(attr) || "" + if path =~ /^\./ + path = File.expand_path(path, base_dir) + self.send("#{attr}=", path) + end + unless File.exists?(path) + STDERR.puts "ERROR: The config option '#{attr}' is set to '#{path}', but the file does not exist!" + exit(1) + end + end + end + + private + + def self.find_file(base_dir, file_path) + return nil unless file_path + if defined? CWD + return File.expand_path(file_path, CWD) if File.exists?(File.expand_path(file_path, CWD)) + end + return File.expand_path(file_path, base_dir) if File.exists?(File.expand_path(file_path, base_dir)) + return nil + end + + def self.symbolize_keys(hsh) + newhsh = {} + hsh.keys.each do |key| + newhsh[key.to_sym] = hsh[key] + end + newhsh + end + end +end diff --git a/lib/leap_ca/couch_changes.rb b/lib/leap_ca/couch_changes.rb index 59209a4..1777eea 100644 --- a/lib/leap_ca/couch_changes.rb +++ b/lib/leap_ca/couch_changes.rb @@ -1,17 +1,19 @@ -class CouchChanges - def initialize(stream) - @stream = stream - end +module LeapCA + class CouchChanges + def initialize(stream) + @stream = stream + end - def last_seq - @stream.get "_changes", :limit => 1, :descending => true do |hash| - return hash[:last_seq] + def last_seq + @stream.get "_changes", :limit => 1, :descending => true do |hash| + return hash[:last_seq] + end end - end - def follow - @stream.get "_changes", :feed => :continuous, :since => last_seq do |hash| - yield(hash) + def follow + @stream.get "_changes", :feed => :continuous, :since => last_seq do |hash| + yield(hash) + end end end -end +end \ No newline at end of file diff --git a/lib/leap_ca/couch_stream.rb b/lib/leap_ca/couch_stream.rb index ed56db2..0c28817 100644 --- a/lib/leap_ca/couch_stream.rb +++ b/lib/leap_ca/couch_stream.rb @@ -1,21 +1,25 @@ -class CouchStream - def initialize(database_url) - @database_url = database_url - end +require 'yajl/http_stream' - def get(path, options) - url = url_for(path, options) - # puts url - Yajl::HttpStream.get(url, :symbolize_keys => true) do |hash| - yield(hash) +module LeapCA + class CouchStream + def initialize(database_url) + @database_url = database_url end - end - protected + def get(path, options) + url = url_for(path, options) + # puts url + Yajl::HttpStream.get(url, :symbolize_keys => true) do |hash| + yield(hash) + end + end - def url_for(path, options = {}) - url = [@database_url, path].join('/') - url += '?' if options.any? - url += options.map {|k,v| "#{k}=#{v}"}.join('&') + protected + + def url_for(path, options = {}) + url = [@database_url, path].join('/') + url += '?' if options.any? + url += options.map {|k,v| "#{k}=#{v}"}.join('&') + end end -end +end \ No newline at end of file diff --git a/lib/leap_ca/pool.rb b/lib/leap_ca/pool.rb index 76c1963..c80206a 100644 --- a/lib/leap_ca/pool.rb +++ b/lib/leap_ca/pool.rb @@ -2,18 +2,19 @@ require 'yaml' module LeapCA class Pool - def initialize(filename) - @config = YAML.load(File.open(filename, 'r')) + def initialize(config = {:size => 10}) + @config = config end def fill while Cert.count < self.size do - Cert.create! + cert = Cert.create! + puts " * Created client certificate #{cert.id}" end end def size - @config[:size] ||= 10 + @config[:size] end end end diff --git a/lib/leap_ca/version.rb b/lib/leap_ca/version.rb index 47b9df2..3af1e72 100644 --- a/lib/leap_ca/version.rb +++ b/lib/leap_ca/version.rb @@ -1,3 +1,4 @@ module LeapCA - VERSION = "0.0.1" + VERSION = "0.1.0" + REQUIRE_PATHS = ['lib'] end diff --git a/lib/leap_ca_daemon.rb b/lib/leap_ca_daemon.rb new file mode 100644 index 0000000..79ec36d --- /dev/null +++ b/lib/leap_ca_daemon.rb @@ -0,0 +1,24 @@ +# +# This file should not be required directly. Use it like so: +# +# Daemons.run('leap_ca_daemon.rb') +# + +require 'leap_ca' + +module LeapCA + puts " * Tracking #{Cert.database.root}" + couch = CouchStream.new(Cert.database.root) + changes = CouchChanges.new(couch) + pool = Pool.new(:size => Config.max_pool_size) + + # fill the pool + pool.fill + + # watch for deletions, fill the pool whenever it gets low + changes.follow do |hash| + if hash[:deleted] + pool.fill + end + end +end -- cgit v1.2.3