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 --- .gitignore | 3 + Capfile | 3 - README.md | 109 +++++++++++++++++++++++++++++++++ Rakefile | 93 ++++++++++++++++++++++++++++ bin/leap_ca | 62 +++++++++++++------ config/config_default.yaml | 32 ++++++++++ config/couchdb.yml.example | 6 -- config/deploy.rb.example | 23 ------- config/pool.yml | 1 - leap_ca.gemspec | 12 ++-- 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 ++++++++ test/config/config.yaml | 20 ++++++ test/files/ca.crt | 14 +++++ test/files/ca.key | 18 ++++++ test/test_helper.rb | 10 ++- test/unit/cert_test.rb | 52 ++++++---------- test/unit/couch_changes_test.rb | 6 +- test/unit/couch_stream_test.rb | 6 +- 25 files changed, 633 insertions(+), 159 deletions(-) create mode 100644 .gitignore delete mode 100644 Capfile create mode 100644 README.md create mode 100644 Rakefile create mode 100644 config/config_default.yaml delete mode 100644 config/couchdb.yml.example delete mode 100644 config/deploy.rb.example delete mode 100644 config/pool.yml create mode 100644 lib/leap_ca/config.rb create mode 100644 lib/leap_ca_daemon.rb create mode 100644 test/config/config.yaml create mode 100644 test/files/ca.crt create mode 100644 test/files/ca.key 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} -- cgit v1.2.3