summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorelijah <elijah@riseup.net>2012-11-12 23:53:51 -0800
committerelijah <elijah@riseup.net>2012-11-12 23:53:51 -0800
commitc37a35df81b2d6becc09f1820240db24c3ec632c (patch)
tree50187e4ab1face237760614ecf844b42efdd51e1
parentc90d30621e042cc3e52ffc87e3491ab110a57e9e (diff)
first fully working version of leap_ca
-rw-r--r--.gitignore3
-rw-r--r--Capfile3
-rw-r--r--README.md109
-rw-r--r--Rakefile93
-rwxr-xr-xbin/leap_ca62
-rw-r--r--config/config_default.yaml32
-rw-r--r--config/couchdb.yml.example6
-rw-r--r--config/deploy.rb.example23
-rw-r--r--config/pool.yml1
-rw-r--r--leap_ca.gemspec12
-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
-rw-r--r--test/config/config.yaml20
-rw-r--r--test/files/ca.crt14
-rw-r--r--test/files/ca.key18
-rw-r--r--test/test_helper.rb10
-rw-r--r--test/unit/cert_test.rb52
-rw-r--r--test/unit/couch_changes_test.rb6
-rw-r--r--test/unit/couch_stream_test.rb6
25 files changed, 633 insertions, 159 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..394c147
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+DEVNOTES
+Gemfile.lock
+pkg \ No newline at end of file
diff --git a/Capfile b/Capfile
deleted file mode 100644
index f3e7334..0000000
--- a/Capfile
+++ /dev/null
@@ -1,3 +0,0 @@
-load 'rubygems'
-load 'railsless-deploy'
-load 'config/deploy'
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..af02f7f
--- /dev/null
+++ b/README.md
@@ -0,0 +1,109 @@
+LEAP Certificate Authority Daemon
+---------------------------------------------------
+
+``leap_ca`` is a background daemon that generates x509 certificates as needed and stores them in CouchDB. You can run ``leap_ca`` on a machine that is not connected to a network, and then periodically connect to sync up the cert database.
+
+* Its only interface with the outside world is a CouchDB connection (defaults to localhost).
+* The daemon monitors changes to the database and fills it with x509 certs as needed.
+* It requires access to a Certificate Authority (in other words, the RSA private key and x509 root certificate, in PEM format).
+
+This program is written in Ruby and is distributed under the following license:
+
+> GNU Affero General Public License
+> Version 3.0 or higher
+> http://www.gnu.org/licenses/agpl-3.0.html
+
+Installation
+---------------------
+
+Prerequisites:
+
+ sudo apt-get install ruby ruby-dev couchdb
+ # if you are running ruby 1.8, you will also need rubygems.
+ # for development, you will also need git, bundle, and rake.
+
+From source:
+
+ git clone git://leap.se/leap_ca
+ cd cleap_ca
+ bundle
+ rake build
+ sudo rake install
+
+From gem:
+
+ sudo gem install leap_ca
+
+Running
+--------------------
+
+See if it worked:
+
+ leap_ca run -- test/config/config.yaml
+ browse to http://localhost:5984/_utils
+
+How you would run normally in production mode:
+
+ leap_ca start
+ leap_ca stop
+
+See ``leap_ca --help`` for more options.
+
+Configuration
+---------------------
+
+``leap_ca`` reads the following configurations files, in this order:
+
+* ``$(leap_ca_source)/config/default_config.yaml``
+* ``/etc/leap/leap_ca.yaml``
+* Any file passed to ARGV like so ``leap_ca start -- /etc/leap_ca.yaml``
+
+Other than ``ca_key_path`` and ``ca_cert_path`` you can probably leave all other options at their default values.
+
+The default options are:
+
+ #
+ # Default configuration options for LEAP Certificate Authority Daemon
+ #
+
+ #
+ # Certificate Authority
+ #
+ ca_key_path: "../test/files/ca.key"
+ ca_key_password: nil
+ ca_cert_path: "../test/files/ca.crt"
+
+ #
+ # Certificate pool
+ #
+ max_pool_size: 100
+ client_cert_lifespan: 2
+ client_cert_bit_size: 2024
+ client_cert_hash: "SHA256"
+
+ #
+ # Database
+ #
+ db_name: "client_certificates"
+ couch_connection:
+ protocol: "http"
+ host: "localhost"
+ port: 5984
+ username: ~
+ password: ~
+ prefix: ""
+ suffix: ""
+
+Rake Tasks
+----------------------------
+
+ rake -T
+ rake build # Build leap_ca-x.x.x.gem into the pkg directory
+ rake install # Install leap_ca-x.x.x.gem into either system-wide or user gems
+ rake test # Run tests
+ rake uninstall # Uninstall leap_ca-x.x.x.gem from either system-wide or user gems
+
+Todo
+----------------------------
+
+* Remove deprecated 'yajl/http_stream'
diff --git a/Rakefile b/Rakefile
new file mode 100644
index 0000000..e28f38f
--- /dev/null
+++ b/Rakefile
@@ -0,0 +1,93 @@
+require "rubygems"
+require "highline/import"
+require "pty"
+require "fileutils"
+require 'rake/testtask'
+
+##
+## HELPER
+##
+
+def run(cmd)
+ PTY.spawn(cmd) do |output, input, pid|
+ begin
+ while line = output.gets do
+ puts line
+ end
+ rescue Errno::EIO
+ end
+ end
+rescue PTY::ChildExited
+end
+
+##
+## GEM BUILDING AND INSTALLING
+##
+
+$spec_path = 'leap_ca.gemspec'
+$spec = eval(File.read($spec_path))
+$base_dir = File.dirname(__FILE__)
+$gem_path = File.join($base_dir, 'pkg', "#{$spec.name}-#{$spec.version}.gem")
+
+def built_gem_path
+ Dir[File.join($base_dir, "#{$spec.name}-*.gem")].sort_by{|f| File.mtime(f)}.last
+end
+
+desc "Build #{$spec.name}-#{$spec.version}.gem into the pkg directory"
+task 'build' do
+ FileUtils.mkdir_p(File.join($base_dir, 'pkg'))
+ FileUtils.rm($gem_path) if File.exists?($gem_path)
+ run "gem build -V '#{$spec_path}'"
+ file_name = File.basename(built_gem_path)
+ FileUtils.mv(built_gem_path, 'pkg')
+ say "#{$spec.name} #{$spec.version} built to pkg/#{file_name}"
+end
+
+desc "Install #{$spec.name}-#{$spec.version}.gem into either system-wide or user gems"
+task 'install' do
+ if !File.exists?($gem_path)
+ say("Could not file #{$gem_path}. Try running 'rake build'")
+ else
+ if ENV["USER"] == "root"
+ run "gem install '#{$gem_path}'"
+ else
+ home_gem_path = Gem.path.grep(/home/).first
+ say("You are installing as an unprivileged user, which will result in the installation being placed in '#{home_gem_path}'.")
+ if agree("Do you want to continue installing to #{home_gem_path}? ")
+ run "gem install '#{$gem_path}' --user-install"
+ end
+ end
+ end
+end
+
+desc "Uninstall #{$spec.name}-#{$spec.version}.gem from either system-wide or user gems"
+task 'uninstall' do
+ if ENV["USER"] == "root"
+ say("Removing #{$spec.name}-#{$spec.version}.gem from system-wide gems")
+ run "gem uninstall '#{$spec.name}' --version #{$spec.version} --verbose -x -I"
+ else
+ say("Removing #{$spec.name}-#{$spec.version}.gem from user's gems")
+ run "gem uninstall '#{$spec.name}' --version #{$spec.version} --verbose --user-install -x -I"
+ end
+end
+
+##
+## TESTING
+##
+
+Rake::TestTask.new do |t|
+ t.pattern = "test/unit/*_test.rb"
+end
+task :default => :test
+
+##
+## DOCUMENTATION
+##
+
+# require 'rdoc/task'
+
+# Rake::RDocTask.new do |rd|
+# rd.main = "README.rdoc"
+# rd.rdoc_files.include("README.rdoc","lib/**/*.rb","bin/**/*")
+# rd.title = 'Your application title'
+# end
diff --git a/bin/leap_ca b/bin/leap_ca
index f999238..0234c15 100755
--- a/bin/leap_ca
+++ b/bin/leap_ca
@@ -1,23 +1,51 @@
#!/usr/bin/ruby
-LEAP_CA_ROOT = File.expand_path('../..', __FILE__)
-$:.unshift File.expand_path('lib', LEAP_CA_ROOT)
-require 'rubygems'
-require 'daemons'
-require 'yajl/http_stream'
+#
+# LEAP Client Certificate Generation Daemon
+#
-require 'leap_ca'
+BASE_DIR = File.expand_path('../..', File.symlink?(__FILE__) ? File.readlink(__FILE__) : __FILE__)
-puts "Tracking #{Cert.database.root}"
-couch = CouchStream.new(Cert.database.root)
-changes = CouchChanges.new(couch)
-pool = LeapCA::Pool.new(File.expand_path("config/pool.yml", LEAP_CA_ROOT))
-pool.fill
-Daemons.run_proc('leap_ca.rb') do
- changes.follow do |hash|
- p hash
- if hash[:deleted]
- pool.fill
- end
+begin
+ #
+ # try without rubygems (might be already loaded or not present)
+ #
+ require 'leap_ca/version'
+rescue LoadError
+ #
+ # try with rubygems
+ #
+ require "#{BASE_DIR}/lib/leap_ca/version.rb"
+ LeapCA::REQUIRE_PATHS.each do |path|
+ path = File.expand_path(path, BASE_DIR)
+ $LOAD_PATH.unshift path unless $LOAD_PATH.include?(path)
end
+ require 'rubygems'
+ require 'leap_ca/version'
+end
+
+# Graceful Ctrl-C
+Signal.trap("SIGINT") do
+ puts "\nQuit"
+ exit
+end
+
+# this changes later, so save the initial current directory
+CWD = Dir.pwd
+
+# handle --version ourselves
+if ARGV.grep(/--version/).any?
+ puts "leap_ca #{LeapCA::VERSION}, ruby #{RUBY_VERSION}"
+ exit(0)
+end
+
+#
+# Start the daemon
+#
+require 'daemons'
+if ENV["USER"] == "root"
+ options = {:app_name => 'leap_ca', :dir_mode => :system} # this will put the pid file in /var/run
+else
+ options = {:app_name => 'leap_ca', :dir_mode => :normal, :dir => '/tmp'} # this will put the pid file in /tmp
end
+Daemons.run("#{BASE_DIR}/lib/leap_ca_daemon.rb", options)
diff --git a/config/config_default.yaml b/config/config_default.yaml
new file mode 100644
index 0000000..357a1f9
--- /dev/null
+++ b/config/config_default.yaml
@@ -0,0 +1,32 @@
+#
+# Default configuration options for LEAP Certificate Authority Daemon
+#
+
+#
+# Certificate Authority
+#
+ca_key_path: "./test/files/ca.key"
+ca_key_password: nil
+ca_cert_path: "./test/files/ca.crt"
+
+#
+# Certificate pool
+#
+max_pool_size: 100
+client_cert_lifespan: 2
+client_cert_bit_size: 2024
+client_cert_hash: "SHA256"
+
+#
+# Database
+#
+db_name: "client_certificates"
+couch_connection:
+ protocol: "http"
+ host: "localhost"
+ port: 5984
+ username: ~
+ password: ~
+ prefix: ""
+ suffix: ""
+ join: "_" \ No newline at end of file
diff --git a/config/couchdb.yml.example b/config/couchdb.yml.example
deleted file mode 100644
index 64058da..0000000
--- a/config/couchdb.yml.example
+++ /dev/null
@@ -1,6 +0,0 @@
-development:
- protocol: 'http'
- host: 'localhost'
- port: 5984
- prefix: leap_web_demo
- suffix: ''
diff --git a/config/deploy.rb.example b/config/deploy.rb.example
deleted file mode 100644
index 5fb18ed..0000000
--- a/config/deploy.rb.example
+++ /dev/null
@@ -1,23 +0,0 @@
-require "bundler/capistrano"
-
-set :application, "leap_ca"
-
-set :scm, :git
-set :repository, "git://leap.se/leap_ca"
-
-role :app, "YOUR SERVER GOES HERE"
-
-# if you want to clean up old releases on each deploy uncomment this:
-# after "deploy:restart", "deploy:cleanup"
-
-namespace :deploy do
- task :start, :roles => :app do
- run "#{File.join(current_path,'bin','leap_ca')} start"
- end
- task :stop, :roles => :app do
- run "#{File.join(current_path,'bin','leap_ca')} stop"
- end
- task :restart, :roles => :app, :except => { :no_release => true } do
- run "#{File.join(current_path,'bin','leap_ca')} restart"
- end
-end
diff --git a/config/pool.yml b/config/pool.yml
deleted file mode 100644
index 29ccbbf..0000000
--- a/config/pool.yml
+++ /dev/null
@@ -1 +0,0 @@
-size: 10
diff --git a/leap_ca.gemspec b/leap_ca.gemspec
index 43c2e27..1b412ac 100644
--- a/leap_ca.gemspec
+++ b/leap_ca.gemspec
@@ -9,18 +9,20 @@ Gem::Specification.new do |s|
s.version = LeapCA::VERSION
s.authors = ["Azul"]
s.email = ["azul@leap.se"]
- s.homepage = "http://www.leap.se"
- s.summary = "CA deamon for the leap platform"
- s.description = "This deamon refills the pool of client certs for the leap platform. They are stored in a CouchDB instance and can be handed out with a webservice."
+ s.homepage = "https://leap.se"
+ s.summary = "Certificate Authority deamon for the LEAP Platform"
+ s.description = "Provides the executable leap_ca, a deamon that refills a pool of x509 client certs stored in CouchDB."
- s.files = Dir["{config,lib}/**/*", 'bin/*'] + ["Rakefile", "Readme.md"]
+ s.files = Dir["{config,lib}/**/*", 'bin/*'] + ["Rakefile", "README.md"]
s.test_files = Dir["test/**/*"]
+ s.bindir = 'bin'
+ s.executables << 'leap_ca'
s.add_dependency "couchrest", "~> 1.1.3"
s.add_dependency "couchrest_model", "~> 2.0.0.beta2"
s.add_dependency "daemons"
s.add_dependency "yajl-ruby"
+ s.add_dependency "certificate_authority"
s.add_development_dependency "minitest", "~> 3.2.0"
s.add_development_dependency "mocha"
-
end
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
diff --git a/test/config/config.yaml b/test/config/config.yaml
new file mode 100644
index 0000000..707385c
--- /dev/null
+++ b/test/config/config.yaml
@@ -0,0 +1,20 @@
+#
+# testing configuration options
+#
+
+#
+# Certificate Authority
+#
+ca_key_path: "./test/files/ca.key"
+ca_key_password: ~
+ca_cert_path: "./test/files/ca.crt"
+
+#
+# Certificate pool
+#
+max_pool_size: 4
+client_cert_lifespan: 1
+client_cert_bit_size: 1024
+client_cert_hash: "SHA1"
+
+db_name: "client_certificates_test"
diff --git a/test/files/ca.crt b/test/files/ca.crt
new file mode 100644
index 0000000..cade598
--- /dev/null
+++ b/test/files/ca.crt
@@ -0,0 +1,14 @@
+-----BEGIN CERTIFICATE-----
+MIICPDCCAYmgAwIBAgIEUKCI4DANBgkqhkiG9w0BAQsFADAkMSIwIAYDVQQDExlS
+b290IENBIGZvciBydW5uaW5nIHRlc3RzMB4XDTEyMTExMjA1MjgwMFoXDTEzMTEx
+MjA1MjgwMFowJDEiMCAGA1UEAxMZUm9vdCBDQSBmb3IgcnVubmluZyB0ZXN0czCB
+uzANBgkqhkiG9w0BAQEFAAOBqQAwgaUCgZ0ApeqCGQOmiHxCFxsfUKmBV6ruOYar
+EsepFAycTmmakXBjNj4B9Pd3gE3Cc56rvkq0uxluRvqspzpEOQpCg8M5fkft/fxS
+acw+ackj3ys7r0MrXgL66QeLnNGe8+RjBO8UHb3OPx547hqUHVg+3HqSCdn9cGQX
+9//EJrnSJsLuZw9ktkN4Ytyd1deZo6AkiIeCyz0HxKQBIhdJAPRlAgMBAAGjQzBB
+MA8GA1UdEwEB/wQFMAMBAf8wDwYDVR0PAQH/BAUDAwcEADAdBgNVHQ4EFgQUBe1l
+BbuGErEkHLffGvkY5dDOH1YwDQYJKoZIhvcNAQELBQADgZ0ADpudncToYPS183w8
+c68dObCCvNfv/FTBg4ihCLW6PapADYuvXmCvXgHflylET+rFdcrnUfl+XjNT5IjF
+ImUyyOnCiy7scRgY+9qrEb7neH4CopGZKkWBTadZLu0QZqMcsWyAZBzaI8tBwL+G
++ylSgw3xTSf/HFjmTJAlDzUieV4DufrPqz7Yx0GrTswdJOcccc/PWUvQIU1GXvto
+-----END CERTIFICATE-----
diff --git a/test/files/ca.key b/test/files/ca.key
new file mode 100644
index 0000000..d266ef7
--- /dev/null
+++ b/test/files/ca.key
@@ -0,0 +1,18 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIC2gIBAAKBnQCl6oIZA6aIfEIXGx9QqYFXqu45hqsSx6kUDJxOaZqRcGM2PgH0
+93eATcJznqu+SrS7GW5G+qynOkQ5CkKDwzl+R+39/FJpzD5pySPfKzuvQyteAvrp
+B4uc0Z7z5GME7xQdvc4/HnjuGpQdWD7cepIJ2f1wZBf3/8QmudImwu5nD2S2Q3hi
+3J3V15mjoCSIh4LLPQfEpAEiF0kA9GUCAwEAAQKBnAKz9FSgqO42Sq6tBBtAolkh
+nBSXK2L4mmTiOQr/UMOnzLtN0qMBWRK1Bu2dRcz+0zztEs0t45wsfdS0DxYDGy+s
+elBrSOhs/w34IeZ5LM6xY0u4HZDmhn0pQNo6QZcFICr0GkkYdmWDlkLvIeJ/u6+q
+nmyqAQXvj3R4nA7hrKUXzJjfvN3RYrhLN+/T41zLybeJ5vLZQK3jJSiIjQJPAMhS
+HTIbYTUi2pxYVSwJDY4S2klTdroNGvTCkqcTRcB4Ms70FGLPZ6+ZumrkbSohHUsj
+gDRRy3e4fjA9qMSQynVr2gkUobsR0tAdQGVOKwJPANQIUPaTc2ouNYNLAiHoAXoL
+qAcF5g7/vtlMOwr+16EYoG7bLbiEie7nBfg9zz/VUnvOEy6pZ89YvsZOMlGicsRs
++tfUM1g/u0ZFEoQPrwJOC6bbE+ML0G9qj9WDfsA4DZ+DGujD6yZ//uSiax1v3TYg
+nnEMDoNJ4KjscvM+dkjez1QNTP3E+/27OUsc2fIiFJplYEnW7m6m+Hv7FulpAk8A
+tiASk0oiV/ErLARw53jmU9PRV378lqOcZgAxswclZo3FuJLxmc3WwOuV2B4Xd+gf
+epKPLYR708GR1Lp0RGS6GfjWGi9+ju3nSbuo5OCnAk5yun/UvDdtnZ6fXo9aF22/
+yoiztru7yhJdVrMx3PbbndfN2y9ctqcd6CD5fIQdyZ4K8eTr686RjH8C0XP095Ib
+an3AO/TQG1c4yE2hSvQ=
+-----END RSA PRIVATE KEY-----
diff --git a/test/test_helper.rb b/test/test_helper.rb
index d78cc96..7e90cfa 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -1,6 +1,10 @@
require 'rubygems'
require 'minitest/autorun'
-LEAP_CA_ROOT = File.expand_path('../..', __FILE__)
-$:.unshift File.expand_path('lib', LEAP_CA_ROOT)
-require 'mocha'
+BASE_DIR = File.expand_path('../..', __FILE__)
+$:.unshift File.expand_path('lib', BASE_DIR)
+
+require 'mocha/setup'
+
+LEAP_CA_CONFIG = "test/config/config.yaml"
+require 'leap_ca'
diff --git a/test/unit/cert_test.rb b/test/unit/cert_test.rb
index 761e5a9..21e4d96 100644
--- a/test/unit/cert_test.rb
+++ b/test/unit/cert_test.rb
@@ -1,50 +1,32 @@
-require 'test_helper'
-require 'leap_ca/cert'
+require File.expand_path('../../test_helper.rb', __FILE__)
class CertTest < MiniTest::Unit::TestCase
def setup
- @sample = Cert.new
- @sample.set_random
- @sample.attach_zip
+ @cert = LeapCA::Cert.new
end
- def test_certs_come_with_attachments
- assert @sample.has_attachment? "cert.txt"
- end
+ def test_generate
+ @cert.generate
- def test_zipper_returns_zip_attachement
- assert_equal "text/plain", @sample.zip_attachment["content_type"]
- end
+ assert @cert.cert, 'certificate should exist'
+ assert @cert.key, 'key should exist'
- def test_zipname_returns_name_of_zip_file
- assert_equal "cert.txt", @sample.zipname
- end
+ ca = OpenSSL::X509::Certificate.new(File.read(LeapCA::Config.ca_cert_path))
+ cert = OpenSSL::X509::Certificate.new(@cert.cert)
+ key = OpenSSL::PKey::RSA.new(@cert.key)
- def test_test_data
- assert @sample.valid?
- end
-
- def test_zipped_returns_actual_data
- @sample.save # This is required!
- lines = @sample.zipped.split("\n")
- assert_equal 56, lines.count
- assert_equal "-----BEGIN RSA PRIVATE KEY-----", lines.first.chomp
- assert_equal "-----END CERTIFICATE-----", lines.last.chomp
+ assert cert.verify(ca.public_key), "cert was not signed by CA"
+ assert_equal ca.subject.to_s, cert.issuer.to_s, 'issuer should match'
+ assert_equal "test", cert.public_key.public_decrypt(key.private_encrypt("test")), 'keypair should be able to encrypt/decrypt'
end
def test_validation_of_random
- @sample.stubs(:set_random)
- [0, 1, nil, "asdf"].each do |invalid|
- @sample.random = invalid
- assert !@sample.valid?, "#{invalid} should not be a valid value for random"
+ @cert.stubs(:set_random)
+ [1, nil, "asdf"].each do |invalid|
+ @cert.random = invalid
+ assert !@cert.valid?, "#{invalid} should not be a valid value for random"
end
end
- def test_validation_of_attachement
- @sample.stubs(:attach_zip)
- @sample.delete_attachment(@sample.zipname)
- assert !@sample.valid?, "Cert should require zipped attachment"
- end
-
-end
+end \ No newline at end of file
diff --git a/test/unit/couch_changes_test.rb b/test/unit/couch_changes_test.rb
index 2ef5de3..9c99d30 100644
--- a/test/unit/couch_changes_test.rb
+++ b/test/unit/couch_changes_test.rb
@@ -1,5 +1,5 @@
-require 'test_helper'
-require 'lib/couch_changes'
+require File.expand_path('../../test_helper.rb', __FILE__)
+require 'leap_ca/couch_changes'
class CouchChangesTest < MiniTest::Unit::TestCase
@@ -7,7 +7,7 @@ class CouchChangesTest < MiniTest::Unit::TestCase
def setup
@stream = mock()
- @changes = CouchChanges.new(@stream)
+ @changes = LeapCA::CouchChanges.new(@stream)
end
def test_last_seq
diff --git a/test/unit/couch_stream_test.rb b/test/unit/couch_stream_test.rb
index af5a34e..7f86351 100644
--- a/test/unit/couch_stream_test.rb
+++ b/test/unit/couch_stream_test.rb
@@ -1,5 +1,5 @@
-require 'test_helper'
-require 'lib/couch_stream'
+require File.expand_path('../../test_helper.rb', __FILE__)
+require 'leap_ca/couch_stream'
# we'll mock this
module Yajl
@@ -11,7 +11,7 @@ class CouchStreamTest < MiniTest::Unit::TestCase
def setup
@root = "http://server/database"
- @stream = CouchStream.new(@root)
+ @stream = LeapCA::CouchStream.new(@root)
@url = @root + "/_changes?a=b&c=d"
@path = "_changes"
@options = {:a => :b, :c => :d}