summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/leap_ca.rb22
-rw-r--r--lib/leap_ca/cert.rb131
-rw-r--r--lib/leap_ca/config.rb71
-rw-r--r--lib/leap_ca/couch_changes.rb26
-rw-r--r--lib/leap_ca/couch_stream.rb36
-rw-r--r--lib/leap_ca/pool.rb9
-rw-r--r--lib/leap_ca/version.rb3
-rw-r--r--lib/leap_ca_daemon.rb24
8 files changed, 262 insertions, 60 deletions
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