leap_ca 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +118 -0
- data/Rakefile +93 -0
- data/bin/leap_ca_daemon +59 -0
- data/config/config_default.yaml +32 -0
- data/lib/leap_ca/cert.rb +145 -0
- data/lib/leap_ca/config.rb +71 -0
- data/lib/leap_ca/couch_changes.rb +19 -0
- data/lib/leap_ca/couch_stream.rb +25 -0
- data/lib/leap_ca/pool.rb +20 -0
- data/lib/leap_ca/version.rb +4 -0
- data/lib/leap_ca.rb +26 -0
- data/lib/leap_ca_daemon.rb +24 -0
- data/test/config/config.yaml +20 -0
- data/test/files/ca.crt +14 -0
- data/test/files/ca.key +18 -0
- data/test/test_helper.rb +10 -0
- data/test/unit/cert_test.rb +32 -0
- data/test/unit/couch_changes_test.rb +32 -0
- data/test/unit/couch_stream_test.rb +35 -0
- metadata +217 -0
data/README.md
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
LEAP Certificate Authority Daemon
|
2
|
+
---------------------------------------------------
|
3
|
+
|
4
|
+
``leap_ca_daemon`` 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.
|
5
|
+
|
6
|
+
* Its only interface with the outside world is a CouchDB connection (defaults to localhost).
|
7
|
+
* The daemon monitors changes to the database and fills it with x509 certs as needed.
|
8
|
+
* It requires access to a Certificate Authority (in other words, the RSA private key and x509 root certificate, in PEM format).
|
9
|
+
|
10
|
+
This program is written in Ruby and is distributed under the following license:
|
11
|
+
|
12
|
+
> GNU Affero General Public License
|
13
|
+
> Version 3.0 or higher
|
14
|
+
> http://www.gnu.org/licenses/agpl-3.0.html
|
15
|
+
|
16
|
+
Installation
|
17
|
+
---------------------
|
18
|
+
|
19
|
+
Prerequisites:
|
20
|
+
|
21
|
+
sudo apt-get install ruby ruby-dev couchdb
|
22
|
+
# if you are running ruby 1.8, you will also need rubygems.
|
23
|
+
# for development, you will also need git, bundle, and rake.
|
24
|
+
|
25
|
+
From source:
|
26
|
+
|
27
|
+
git clone git://leap.se/leap_ca
|
28
|
+
cd cleap_ca
|
29
|
+
bundle
|
30
|
+
rake build
|
31
|
+
sudo rake install
|
32
|
+
|
33
|
+
From gem:
|
34
|
+
|
35
|
+
sudo gem install leap_ca
|
36
|
+
|
37
|
+
Running
|
38
|
+
--------------------
|
39
|
+
|
40
|
+
See if it worked:
|
41
|
+
|
42
|
+
leap_ca_daemon run -- test/config/config.yaml
|
43
|
+
browse to http://localhost:5984/_utils
|
44
|
+
|
45
|
+
How you would run normally in production mode:
|
46
|
+
|
47
|
+
leap_ca_daemon start
|
48
|
+
leap_ca_daemon stop
|
49
|
+
|
50
|
+
See ``leap_ca_daemon --help`` for more options.
|
51
|
+
|
52
|
+
Configuration
|
53
|
+
---------------------
|
54
|
+
|
55
|
+
``leap_ca_daemon`` reads the following configurations files, in this order:
|
56
|
+
|
57
|
+
* ``$(leap_ca_source)/config/default_config.yaml``
|
58
|
+
* ``/etc/leap/leap_ca.yaml``
|
59
|
+
* Any file passed to ARGV like so ``leap_ca start -- /etc/leap_ca.yaml``
|
60
|
+
|
61
|
+
Other than ``ca_key_path`` and ``ca_cert_path`` you can probably leave all other options at their default values.
|
62
|
+
|
63
|
+
The default options are:
|
64
|
+
|
65
|
+
#
|
66
|
+
# Default configuration options for LEAP Certificate Authority Daemon
|
67
|
+
#
|
68
|
+
|
69
|
+
#
|
70
|
+
# Certificate Authority
|
71
|
+
#
|
72
|
+
ca_key_path: "../test/files/ca.key"
|
73
|
+
ca_key_password: nil
|
74
|
+
ca_cert_path: "../test/files/ca.crt"
|
75
|
+
|
76
|
+
#
|
77
|
+
# Certificate pool
|
78
|
+
#
|
79
|
+
max_pool_size: 100
|
80
|
+
client_cert_lifespan: 2
|
81
|
+
client_cert_bit_size: 2024
|
82
|
+
client_cert_hash: "SHA256"
|
83
|
+
|
84
|
+
#
|
85
|
+
# Database
|
86
|
+
#
|
87
|
+
db_name: "client_certificates"
|
88
|
+
couch_connection:
|
89
|
+
protocol: "http"
|
90
|
+
host: "localhost"
|
91
|
+
port: 5984
|
92
|
+
username: ~
|
93
|
+
password: ~
|
94
|
+
prefix: ""
|
95
|
+
suffix: ""
|
96
|
+
|
97
|
+
Rake Tasks
|
98
|
+
----------------------------
|
99
|
+
|
100
|
+
rake -T
|
101
|
+
rake build # Build leap_ca-x.x.x.gem into the pkg directory
|
102
|
+
rake install # Install leap_ca-x.x.x.gem into either system-wide or user gems
|
103
|
+
rake test # Run tests
|
104
|
+
rake uninstall # Uninstall leap_ca-x.x.x.gem from either system-wide or user gems
|
105
|
+
|
106
|
+
Development
|
107
|
+
--------------------
|
108
|
+
|
109
|
+
For development and debugging you might want to run the programm directly without
|
110
|
+
the deamon wrapper. You can do this like this:
|
111
|
+
|
112
|
+
ruby -I lib lib/leap_ca_daemon.rb
|
113
|
+
|
114
|
+
|
115
|
+
Todo
|
116
|
+
----------------------------
|
117
|
+
|
118
|
+
* Remove deprecated 'yajl/http_stream'
|
data/Rakefile
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
require "highline/import"
|
3
|
+
require "pty"
|
4
|
+
require "fileutils"
|
5
|
+
require 'rake/testtask'
|
6
|
+
|
7
|
+
##
|
8
|
+
## HELPER
|
9
|
+
##
|
10
|
+
|
11
|
+
def run(cmd)
|
12
|
+
PTY.spawn(cmd) do |output, input, pid|
|
13
|
+
begin
|
14
|
+
while line = output.gets do
|
15
|
+
puts line
|
16
|
+
end
|
17
|
+
rescue Errno::EIO
|
18
|
+
end
|
19
|
+
end
|
20
|
+
rescue PTY::ChildExited
|
21
|
+
end
|
22
|
+
|
23
|
+
##
|
24
|
+
## GEM BUILDING AND INSTALLING
|
25
|
+
##
|
26
|
+
|
27
|
+
$spec_path = 'leap_ca.gemspec'
|
28
|
+
$spec = eval(File.read($spec_path))
|
29
|
+
$base_dir = File.dirname(__FILE__)
|
30
|
+
$gem_path = File.join($base_dir, 'pkg', "#{$spec.name}-#{$spec.version}.gem")
|
31
|
+
|
32
|
+
def built_gem_path
|
33
|
+
Dir[File.join($base_dir, "#{$spec.name}-*.gem")].sort_by{|f| File.mtime(f)}.last
|
34
|
+
end
|
35
|
+
|
36
|
+
desc "Build #{$spec.name}-#{$spec.version}.gem into the pkg directory"
|
37
|
+
task 'build' do
|
38
|
+
FileUtils.mkdir_p(File.join($base_dir, 'pkg'))
|
39
|
+
FileUtils.rm($gem_path) if File.exists?($gem_path)
|
40
|
+
run "gem build -V '#{$spec_path}'"
|
41
|
+
file_name = File.basename(built_gem_path)
|
42
|
+
FileUtils.mv(built_gem_path, 'pkg')
|
43
|
+
say "#{$spec.name} #{$spec.version} built to pkg/#{file_name}"
|
44
|
+
end
|
45
|
+
|
46
|
+
desc "Install #{$spec.name}-#{$spec.version}.gem into either system-wide or user gems"
|
47
|
+
task 'install' do
|
48
|
+
if !File.exists?($gem_path)
|
49
|
+
say("Could not file #{$gem_path}. Try running 'rake build'")
|
50
|
+
else
|
51
|
+
if ENV["USER"] == "root"
|
52
|
+
run "gem install '#{$gem_path}'"
|
53
|
+
else
|
54
|
+
home_gem_path = Gem.path.grep(/home/).first
|
55
|
+
say("You are installing as an unprivileged user, which will result in the installation being placed in '#{home_gem_path}'.")
|
56
|
+
if agree("Do you want to continue installing to #{home_gem_path}? ")
|
57
|
+
run "gem install '#{$gem_path}' --user-install"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
desc "Uninstall #{$spec.name}-#{$spec.version}.gem from either system-wide or user gems"
|
64
|
+
task 'uninstall' do
|
65
|
+
if ENV["USER"] == "root"
|
66
|
+
say("Removing #{$spec.name}-#{$spec.version}.gem from system-wide gems")
|
67
|
+
run "gem uninstall '#{$spec.name}' --version #{$spec.version} --verbose -x -I"
|
68
|
+
else
|
69
|
+
say("Removing #{$spec.name}-#{$spec.version}.gem from user's gems")
|
70
|
+
run "gem uninstall '#{$spec.name}' --version #{$spec.version} --verbose --user-install -x -I"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
##
|
75
|
+
## TESTING
|
76
|
+
##
|
77
|
+
|
78
|
+
Rake::TestTask.new do |t|
|
79
|
+
t.pattern = "test/unit/*_test.rb"
|
80
|
+
end
|
81
|
+
task :default => :test
|
82
|
+
|
83
|
+
##
|
84
|
+
## DOCUMENTATION
|
85
|
+
##
|
86
|
+
|
87
|
+
# require 'rdoc/task'
|
88
|
+
|
89
|
+
# Rake::RDocTask.new do |rd|
|
90
|
+
# rd.main = "README.rdoc"
|
91
|
+
# rd.rdoc_files.include("README.rdoc","lib/**/*.rb","bin/**/*")
|
92
|
+
# rd.title = 'Your application title'
|
93
|
+
# end
|
data/bin/leap_ca_daemon
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
|
3
|
+
#
|
4
|
+
# LEAP Client Certificate Generation Daemon
|
5
|
+
#
|
6
|
+
|
7
|
+
BASE_DIR = File.expand_path('../..', File.symlink?(__FILE__) ? File.readlink(__FILE__) : __FILE__)
|
8
|
+
|
9
|
+
begin
|
10
|
+
#
|
11
|
+
# try without rubygems (might be already loaded or not present)
|
12
|
+
#
|
13
|
+
require 'leap_ca/version'
|
14
|
+
rescue LoadError
|
15
|
+
#
|
16
|
+
# try with rubygems
|
17
|
+
#
|
18
|
+
require "#{BASE_DIR}/lib/leap_ca/version.rb"
|
19
|
+
LeapCA::REQUIRE_PATHS.each do |path|
|
20
|
+
path = File.expand_path(path, BASE_DIR)
|
21
|
+
$LOAD_PATH.unshift path unless $LOAD_PATH.include?(path)
|
22
|
+
end
|
23
|
+
require 'rubygems'
|
24
|
+
require 'leap_ca/version'
|
25
|
+
end
|
26
|
+
|
27
|
+
# Graceful Ctrl-C
|
28
|
+
Signal.trap("SIGINT") do
|
29
|
+
puts "\nQuit"
|
30
|
+
exit
|
31
|
+
end
|
32
|
+
|
33
|
+
# this changes later, so save the initial current directory
|
34
|
+
CWD = Dir.pwd
|
35
|
+
|
36
|
+
# handle --version ourselves
|
37
|
+
if ARGV.grep(/--version/).any?
|
38
|
+
puts "leap_ca #{LeapCA::VERSION}, ruby #{RUBY_VERSION}"
|
39
|
+
exit(0)
|
40
|
+
end
|
41
|
+
|
42
|
+
# --fill-pool will fill the pool and then exit
|
43
|
+
if ARGV.grep(/--fill-pool/).any?
|
44
|
+
require 'leap_ca'
|
45
|
+
pool = LeapCA::Pool.new(:size => LeapCA::Config.max_pool_size)
|
46
|
+
pool.fill
|
47
|
+
exit(0)
|
48
|
+
end
|
49
|
+
|
50
|
+
#
|
51
|
+
# Start the daemon
|
52
|
+
#
|
53
|
+
require 'daemons'
|
54
|
+
if ENV["USER"] == "root"
|
55
|
+
options = {:app_name => 'leap_ca', :dir_mode => :system} # this will put the pid file in /var/run
|
56
|
+
else
|
57
|
+
options = {:app_name => 'leap_ca', :dir_mode => :normal, :dir => '/tmp'} # this will put the pid file in /tmp
|
58
|
+
end
|
59
|
+
Daemons.run("#{BASE_DIR}/lib/leap_ca_daemon.rb", options)
|
@@ -0,0 +1,32 @@
|
|
1
|
+
#
|
2
|
+
# Default configuration options for LEAP Certificate Authority Daemon
|
3
|
+
#
|
4
|
+
|
5
|
+
#
|
6
|
+
# Certificate Authority
|
7
|
+
#
|
8
|
+
ca_key_path: "./test/files/ca.key"
|
9
|
+
ca_key_password: nil
|
10
|
+
ca_cert_path: "./test/files/ca.crt"
|
11
|
+
|
12
|
+
#
|
13
|
+
# Certificate pool
|
14
|
+
#
|
15
|
+
max_pool_size: 100
|
16
|
+
client_cert_lifespan: 2
|
17
|
+
client_cert_bit_size: 2024
|
18
|
+
client_cert_hash: "SHA256"
|
19
|
+
|
20
|
+
#
|
21
|
+
# Database
|
22
|
+
#
|
23
|
+
db_name: "client_certificates"
|
24
|
+
couch_connection:
|
25
|
+
protocol: "http"
|
26
|
+
host: "localhost"
|
27
|
+
port: 5984
|
28
|
+
username: ~
|
29
|
+
password: ~
|
30
|
+
prefix: ""
|
31
|
+
suffix: ""
|
32
|
+
join: "_"
|
data/lib/leap_ca/cert.rb
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
#
|
2
|
+
# Model for certificates stored in CouchDB.
|
3
|
+
#
|
4
|
+
# This file must be loaded after Config has been loaded.
|
5
|
+
#
|
6
|
+
|
7
|
+
require 'base64'
|
8
|
+
require 'digest/md5'
|
9
|
+
require 'openssl'
|
10
|
+
require 'certificate_authority'
|
11
|
+
require 'date'
|
12
|
+
|
13
|
+
module LeapCA
|
14
|
+
class Cert < CouchRest::Model::Base
|
15
|
+
|
16
|
+
use_database LeapCA::Config.db_name
|
17
|
+
|
18
|
+
timestamps!
|
19
|
+
|
20
|
+
property :key, String # the client private RSA key
|
21
|
+
property :cert, String # the client x509 certificate, signed by the CA
|
22
|
+
property :valid_until, Time # expiration time of the client certificate
|
23
|
+
property :random, Float, :accessible => false # used to help pick a random cert by the webapp
|
24
|
+
|
25
|
+
validates :key, :presence => true
|
26
|
+
validates :cert, :presence => true
|
27
|
+
validates :random, :presence => true, :numericality => {:greater_than_or_equal_to => 0, :less_than => 1}
|
28
|
+
|
29
|
+
before_validation :generate, :set_random, :on => :create
|
30
|
+
|
31
|
+
design do
|
32
|
+
view :by_random
|
33
|
+
end
|
34
|
+
|
35
|
+
class << self
|
36
|
+
def sample
|
37
|
+
self.by_random.startkey(rand).first || self.by_random.first
|
38
|
+
end
|
39
|
+
|
40
|
+
def pick_from_pool
|
41
|
+
cert = self.sample
|
42
|
+
raise RECORD_NOT_FOUND unless cert
|
43
|
+
cert.destroy
|
44
|
+
return cert
|
45
|
+
rescue RESOURCE_NOT_FOUND
|
46
|
+
retry if self.by_random.count > 0
|
47
|
+
raise RECORD_NOT_FOUND
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
#
|
52
|
+
# generate the private key and client certificate
|
53
|
+
#
|
54
|
+
def generate
|
55
|
+
cert = CertificateAuthority::Certificate.new
|
56
|
+
|
57
|
+
# set subject
|
58
|
+
cert.subject.common_name = random_common_name
|
59
|
+
|
60
|
+
# set expiration
|
61
|
+
self.valid_until = months_from_yesterday(Config.client_cert_lifespan)
|
62
|
+
cert.not_before = yesterday
|
63
|
+
cert.not_after = self.valid_until
|
64
|
+
|
65
|
+
# generate key
|
66
|
+
cert.serial_number.number = cert_serial_number
|
67
|
+
cert.key_material.generate_key(Config.client_cert_bit_size)
|
68
|
+
|
69
|
+
# sign
|
70
|
+
cert.parent = Cert.root_ca
|
71
|
+
cert.sign! client_signing_profile
|
72
|
+
|
73
|
+
self.key = cert.key_material.private_key.to_pem
|
74
|
+
self.cert = cert.to_pem
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def set_random
|
80
|
+
self.random = rand
|
81
|
+
end
|
82
|
+
|
83
|
+
def self.root_ca
|
84
|
+
@root_ca ||= begin
|
85
|
+
crt = File.read(Config.ca_cert_path)
|
86
|
+
key = File.read(Config.ca_key_path)
|
87
|
+
openssl_cert = OpenSSL::X509::Certificate.new(crt)
|
88
|
+
cert = CertificateAuthority::Certificate.from_openssl(openssl_cert)
|
89
|
+
cert.key_material.private_key = OpenSSL::PKey::RSA.new(key, Config.ca_key_password)
|
90
|
+
cert
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
#
|
95
|
+
# For cert serial numbers, we need a non-colliding number less than 160 bits.
|
96
|
+
# md5 will do nicely, since there is no need for a secure hash, just a short one.
|
97
|
+
# (md5 is 128 bits)
|
98
|
+
#
|
99
|
+
def cert_serial_number
|
100
|
+
Digest::MD5.hexdigest("#{rand(10**10)} -- #{Time.now}").to_i(16)
|
101
|
+
end
|
102
|
+
|
103
|
+
#
|
104
|
+
# for the random common name, we need a text string that will be unique across all certs.
|
105
|
+
# ruby 1.8 doesn't have a built-in uuid generator, or we would use SecureRandom.uuid
|
106
|
+
#
|
107
|
+
def random_common_name
|
108
|
+
cert_serial_number.to_s(36)
|
109
|
+
end
|
110
|
+
|
111
|
+
def client_signing_profile
|
112
|
+
{
|
113
|
+
"digest" => Config.client_cert_hash,
|
114
|
+
"extensions" => {
|
115
|
+
"keyUsage" => {
|
116
|
+
"usage" => ["digitalSignature"]
|
117
|
+
},
|
118
|
+
"extendedKeyUsage" => {
|
119
|
+
"usage" => ["clientAuth"]
|
120
|
+
}
|
121
|
+
}
|
122
|
+
}
|
123
|
+
end
|
124
|
+
|
125
|
+
##
|
126
|
+
## TIME HELPERS
|
127
|
+
##
|
128
|
+
## note: we use 'yesterday' instead of 'today', because times are in UTC, and some people on the planet
|
129
|
+
## are behind UTC.
|
130
|
+
##
|
131
|
+
|
132
|
+
def yesterday
|
133
|
+
t = Time.now - 24*24*60
|
134
|
+
Time.utc t.year, t.month, t.day
|
135
|
+
end
|
136
|
+
|
137
|
+
def months_from_yesterday(num)
|
138
|
+
t = yesterday
|
139
|
+
date = Date.new t.year, t.month, t.day
|
140
|
+
date = date >> num # >> is months in the future operator
|
141
|
+
Time.utc date.year, date.month, date.day
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module LeapCA
|
4
|
+
module Config
|
5
|
+
extend self
|
6
|
+
|
7
|
+
attr_accessor :ca_key_path
|
8
|
+
attr_accessor :ca_key_password
|
9
|
+
attr_accessor :ca_cert_path
|
10
|
+
|
11
|
+
attr_accessor :max_pool_size
|
12
|
+
attr_accessor :client_cert_lifespan
|
13
|
+
attr_accessor :client_cert_bit_size
|
14
|
+
attr_accessor :client_cert_hash
|
15
|
+
|
16
|
+
attr_accessor :db_name
|
17
|
+
attr_accessor :couch_connection
|
18
|
+
|
19
|
+
def self.load(base_dir, *configs)
|
20
|
+
configs.each do |file_path|
|
21
|
+
file_path = find_file(base_dir, file_path)
|
22
|
+
next unless file_path
|
23
|
+
puts " * Loading configuration #{file_path}"
|
24
|
+
yml = YAML.load(File.read(file_path))
|
25
|
+
if yml
|
26
|
+
yml.each do |key, value|
|
27
|
+
begin
|
28
|
+
if value.is_a? Hash
|
29
|
+
value = symbolize_keys(value)
|
30
|
+
end
|
31
|
+
self.send("#{key}=", value)
|
32
|
+
rescue NoMethodError => exc
|
33
|
+
STDERR.puts "ERROR in file #{file}, '#{key}' is not a valid option"
|
34
|
+
exit(1)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
[:ca_key_path, :ca_cert_path].each do |attr|
|
40
|
+
path = self.send(attr) || ""
|
41
|
+
if path =~ /^\./
|
42
|
+
path = File.expand_path(path, base_dir)
|
43
|
+
self.send("#{attr}=", path)
|
44
|
+
end
|
45
|
+
unless File.exists?(path)
|
46
|
+
STDERR.puts "ERROR: The config option '#{attr}' is set to '#{path}', but the file does not exist!"
|
47
|
+
exit(1)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def self.find_file(base_dir, file_path)
|
55
|
+
return nil unless file_path
|
56
|
+
if defined? CWD
|
57
|
+
return File.expand_path(file_path, CWD) if File.exists?(File.expand_path(file_path, CWD))
|
58
|
+
end
|
59
|
+
return File.expand_path(file_path, base_dir) if File.exists?(File.expand_path(file_path, base_dir))
|
60
|
+
return nil
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.symbolize_keys(hsh)
|
64
|
+
newhsh = {}
|
65
|
+
hsh.keys.each do |key|
|
66
|
+
newhsh[key.to_sym] = hsh[key]
|
67
|
+
end
|
68
|
+
newhsh
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module LeapCA
|
2
|
+
class CouchChanges
|
3
|
+
def initialize(stream)
|
4
|
+
@stream = stream
|
5
|
+
end
|
6
|
+
|
7
|
+
def last_seq
|
8
|
+
@stream.get "_changes", :limit => 1, :descending => true do |hash|
|
9
|
+
return hash[:last_seq]
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def follow
|
14
|
+
@stream.get "_changes", :feed => :continuous, :since => last_seq do |hash|
|
15
|
+
yield(hash)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'yajl/http_stream'
|
2
|
+
|
3
|
+
module LeapCA
|
4
|
+
class CouchStream
|
5
|
+
def initialize(database_url)
|
6
|
+
@database_url = database_url
|
7
|
+
end
|
8
|
+
|
9
|
+
def get(path, options)
|
10
|
+
url = url_for(path, options)
|
11
|
+
# puts url
|
12
|
+
Yajl::HttpStream.get(url, :symbolize_keys => true) do |hash|
|
13
|
+
yield(hash)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
protected
|
18
|
+
|
19
|
+
def url_for(path, options = {})
|
20
|
+
url = [@database_url, path].join('/')
|
21
|
+
url += '?' if options.any?
|
22
|
+
url += options.map {|k,v| "#{k}=#{v}"}.join('&')
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/leap_ca/pool.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module LeapCA
|
4
|
+
class Pool
|
5
|
+
def initialize(config = {:size => 10})
|
6
|
+
@config = config
|
7
|
+
end
|
8
|
+
|
9
|
+
def fill
|
10
|
+
while Cert.count < self.size do
|
11
|
+
cert = Cert.create!
|
12
|
+
puts " * Created client certificate #{cert.id}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def size
|
17
|
+
@config[:size]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/lib/leap_ca.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
unless defined? BASE_DIR
|
2
|
+
BASE_DIR = File.expand_path('../..', __FILE__)
|
3
|
+
end
|
4
|
+
unless defined? LEAP_CA_CONFIG
|
5
|
+
LEAP_CA_CONFIG = '/etc/leap/leap_ca.yaml'
|
6
|
+
end
|
7
|
+
|
8
|
+
#
|
9
|
+
# Load Config
|
10
|
+
# this must come first, because CouchRest needs the connection defined before the models are defined.
|
11
|
+
#
|
12
|
+
require 'leap_ca/config'
|
13
|
+
LeapCA::Config.load(BASE_DIR, 'config/config_default.yaml', LEAP_CA_CONFIG, ARGV.grep(/\.ya?ml$/).first)
|
14
|
+
|
15
|
+
require 'couchrest_model'
|
16
|
+
CouchRest::Model::Base.configure do |config|
|
17
|
+
config.connection = LeapCA::Config.couch_connection
|
18
|
+
end
|
19
|
+
|
20
|
+
#
|
21
|
+
# Load LeapCA
|
22
|
+
#
|
23
|
+
require 'leap_ca/cert'
|
24
|
+
require 'leap_ca/couch_stream'
|
25
|
+
require 'leap_ca/couch_changes'
|
26
|
+
require 'leap_ca/pool'
|
@@ -0,0 +1,24 @@
|
|
1
|
+
#
|
2
|
+
# This file should not be required directly. Use it like so:
|
3
|
+
#
|
4
|
+
# Daemons.run('leap_ca_daemon.rb')
|
5
|
+
#
|
6
|
+
|
7
|
+
require 'leap_ca'
|
8
|
+
|
9
|
+
module LeapCA
|
10
|
+
puts " * Tracking #{Cert.database.root}"
|
11
|
+
couch = CouchStream.new(Cert.database.root)
|
12
|
+
changes = CouchChanges.new(couch)
|
13
|
+
pool = Pool.new(:size => Config.max_pool_size)
|
14
|
+
|
15
|
+
# fill the pool
|
16
|
+
pool.fill
|
17
|
+
|
18
|
+
# watch for deletions, fill the pool whenever it gets low
|
19
|
+
changes.follow do |hash|
|
20
|
+
if hash[:deleted]
|
21
|
+
pool.fill
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
#
|
2
|
+
# testing configuration options
|
3
|
+
#
|
4
|
+
|
5
|
+
#
|
6
|
+
# Certificate Authority
|
7
|
+
#
|
8
|
+
ca_key_path: "./test/files/ca.key"
|
9
|
+
ca_key_password: ~
|
10
|
+
ca_cert_path: "./test/files/ca.crt"
|
11
|
+
|
12
|
+
#
|
13
|
+
# Certificate pool
|
14
|
+
#
|
15
|
+
max_pool_size: 4
|
16
|
+
client_cert_lifespan: 1
|
17
|
+
client_cert_bit_size: 1024
|
18
|
+
client_cert_hash: "SHA1"
|
19
|
+
|
20
|
+
db_name: "client_certificates_test"
|
data/test/files/ca.crt
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
-----BEGIN CERTIFICATE-----
|
2
|
+
MIICPDCCAYmgAwIBAgIEUKCI4DANBgkqhkiG9w0BAQsFADAkMSIwIAYDVQQDExlS
|
3
|
+
b290IENBIGZvciBydW5uaW5nIHRlc3RzMB4XDTEyMTExMjA1MjgwMFoXDTEzMTEx
|
4
|
+
MjA1MjgwMFowJDEiMCAGA1UEAxMZUm9vdCBDQSBmb3IgcnVubmluZyB0ZXN0czCB
|
5
|
+
uzANBgkqhkiG9w0BAQEFAAOBqQAwgaUCgZ0ApeqCGQOmiHxCFxsfUKmBV6ruOYar
|
6
|
+
EsepFAycTmmakXBjNj4B9Pd3gE3Cc56rvkq0uxluRvqspzpEOQpCg8M5fkft/fxS
|
7
|
+
acw+ackj3ys7r0MrXgL66QeLnNGe8+RjBO8UHb3OPx547hqUHVg+3HqSCdn9cGQX
|
8
|
+
9//EJrnSJsLuZw9ktkN4Ytyd1deZo6AkiIeCyz0HxKQBIhdJAPRlAgMBAAGjQzBB
|
9
|
+
MA8GA1UdEwEB/wQFMAMBAf8wDwYDVR0PAQH/BAUDAwcEADAdBgNVHQ4EFgQUBe1l
|
10
|
+
BbuGErEkHLffGvkY5dDOH1YwDQYJKoZIhvcNAQELBQADgZ0ADpudncToYPS183w8
|
11
|
+
c68dObCCvNfv/FTBg4ihCLW6PapADYuvXmCvXgHflylET+rFdcrnUfl+XjNT5IjF
|
12
|
+
ImUyyOnCiy7scRgY+9qrEb7neH4CopGZKkWBTadZLu0QZqMcsWyAZBzaI8tBwL+G
|
13
|
+
+ylSgw3xTSf/HFjmTJAlDzUieV4DufrPqz7Yx0GrTswdJOcccc/PWUvQIU1GXvto
|
14
|
+
-----END CERTIFICATE-----
|
data/test/files/ca.key
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
-----BEGIN RSA PRIVATE KEY-----
|
2
|
+
MIIC2gIBAAKBnQCl6oIZA6aIfEIXGx9QqYFXqu45hqsSx6kUDJxOaZqRcGM2PgH0
|
3
|
+
93eATcJznqu+SrS7GW5G+qynOkQ5CkKDwzl+R+39/FJpzD5pySPfKzuvQyteAvrp
|
4
|
+
B4uc0Z7z5GME7xQdvc4/HnjuGpQdWD7cepIJ2f1wZBf3/8QmudImwu5nD2S2Q3hi
|
5
|
+
3J3V15mjoCSIh4LLPQfEpAEiF0kA9GUCAwEAAQKBnAKz9FSgqO42Sq6tBBtAolkh
|
6
|
+
nBSXK2L4mmTiOQr/UMOnzLtN0qMBWRK1Bu2dRcz+0zztEs0t45wsfdS0DxYDGy+s
|
7
|
+
elBrSOhs/w34IeZ5LM6xY0u4HZDmhn0pQNo6QZcFICr0GkkYdmWDlkLvIeJ/u6+q
|
8
|
+
nmyqAQXvj3R4nA7hrKUXzJjfvN3RYrhLN+/T41zLybeJ5vLZQK3jJSiIjQJPAMhS
|
9
|
+
HTIbYTUi2pxYVSwJDY4S2klTdroNGvTCkqcTRcB4Ms70FGLPZ6+ZumrkbSohHUsj
|
10
|
+
gDRRy3e4fjA9qMSQynVr2gkUobsR0tAdQGVOKwJPANQIUPaTc2ouNYNLAiHoAXoL
|
11
|
+
qAcF5g7/vtlMOwr+16EYoG7bLbiEie7nBfg9zz/VUnvOEy6pZ89YvsZOMlGicsRs
|
12
|
+
+tfUM1g/u0ZFEoQPrwJOC6bbE+ML0G9qj9WDfsA4DZ+DGujD6yZ//uSiax1v3TYg
|
13
|
+
nnEMDoNJ4KjscvM+dkjez1QNTP3E+/27OUsc2fIiFJplYEnW7m6m+Hv7FulpAk8A
|
14
|
+
tiASk0oiV/ErLARw53jmU9PRV378lqOcZgAxswclZo3FuJLxmc3WwOuV2B4Xd+gf
|
15
|
+
epKPLYR708GR1Lp0RGS6GfjWGi9+ju3nSbuo5OCnAk5yun/UvDdtnZ6fXo9aF22/
|
16
|
+
yoiztru7yhJdVrMx3PbbndfN2y9ctqcd6CD5fIQdyZ4K8eTr686RjH8C0XP095Ib
|
17
|
+
an3AO/TQG1c4yE2hSvQ=
|
18
|
+
-----END RSA PRIVATE KEY-----
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require File.expand_path('../../test_helper.rb', __FILE__)
|
2
|
+
|
3
|
+
class CertTest < MiniTest::Unit::TestCase
|
4
|
+
|
5
|
+
def setup
|
6
|
+
@cert = LeapCA::Cert.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def test_generate
|
10
|
+
@cert.generate
|
11
|
+
|
12
|
+
assert @cert.cert, 'certificate should exist'
|
13
|
+
assert @cert.key, 'key should exist'
|
14
|
+
|
15
|
+
ca = OpenSSL::X509::Certificate.new(File.read(LeapCA::Config.ca_cert_path))
|
16
|
+
cert = OpenSSL::X509::Certificate.new(@cert.cert)
|
17
|
+
key = OpenSSL::PKey::RSA.new(@cert.key)
|
18
|
+
|
19
|
+
assert cert.verify(ca.public_key), "cert was not signed by CA"
|
20
|
+
assert_equal ca.subject.to_s, cert.issuer.to_s, 'issuer should match'
|
21
|
+
assert_equal "test", cert.public_key.public_decrypt(key.private_encrypt("test")), 'keypair should be able to encrypt/decrypt'
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_validation_of_random
|
25
|
+
@cert.stubs(:set_random)
|
26
|
+
[1, nil, "asdf"].each do |invalid|
|
27
|
+
@cert.random = invalid
|
28
|
+
assert !@cert.valid?, "#{invalid} should not be a valid value for random"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require File.expand_path('../../test_helper.rb', __FILE__)
|
2
|
+
require 'leap_ca/couch_changes'
|
3
|
+
|
4
|
+
class CouchChangesTest < MiniTest::Unit::TestCase
|
5
|
+
|
6
|
+
LAST_SEQ = 12
|
7
|
+
|
8
|
+
def setup
|
9
|
+
@stream = mock()
|
10
|
+
@changes = LeapCA::CouchChanges.new(@stream)
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_last_seq
|
14
|
+
@stream.expects(:get).
|
15
|
+
with('_changes', {:limit => 1, :descending => true}).
|
16
|
+
yields(:last_seq => LAST_SEQ)
|
17
|
+
assert_equal LAST_SEQ, @changes.last_seq
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_follow
|
21
|
+
stub_entry = {:new => :result}
|
22
|
+
@stream.expects(:get).
|
23
|
+
with('_changes', {:limit => 1, :descending => true}).
|
24
|
+
yields(:last_seq => LAST_SEQ)
|
25
|
+
@stream.expects(:get).
|
26
|
+
with('_changes', {:feed => :continuous, :since => LAST_SEQ}).
|
27
|
+
yields(stub_entry)
|
28
|
+
@changes.follow do |hash|
|
29
|
+
assert_equal stub_entry, hash
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require File.expand_path('../../test_helper.rb', __FILE__)
|
2
|
+
require 'leap_ca/couch_stream'
|
3
|
+
|
4
|
+
# we'll mock this
|
5
|
+
module Yajl
|
6
|
+
class HttpStream
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class CouchStreamTest < MiniTest::Unit::TestCase
|
11
|
+
|
12
|
+
def setup
|
13
|
+
@root = "http://server/database"
|
14
|
+
@stream = LeapCA::CouchStream.new(@root)
|
15
|
+
@url = @root + "/_changes?a=b&c=d"
|
16
|
+
@path = "_changes"
|
17
|
+
@options = {:a => :b, :c => :d}
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_get
|
21
|
+
Yajl::HttpStream.expects(:get).
|
22
|
+
with(@url, :symbolize_keys => true).
|
23
|
+
yields(stub_hash = stub)
|
24
|
+
@stream.get(@path, @options) do |hash|
|
25
|
+
assert_equal stub_hash, hash
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# internal
|
30
|
+
def test_url_creation
|
31
|
+
assert_equal "http://server/database/", @stream.send(:url_for, "")
|
32
|
+
assert_equal @url, @stream.send(:url_for, @path, @options)
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
metadata
ADDED
@@ -0,0 +1,217 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: leap_ca
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Azul
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-12-31 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: couchrest
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 1.1.3
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 1.1.3
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: couchrest_model
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 2.0.0.beta2
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 2.0.0.beta2
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: daemons
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: yajl-ruby
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :runtime
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: certificate_authority
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :runtime
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: minitest
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ~>
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: 3.2.0
|
102
|
+
type: :development
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ~>
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: 3.2.0
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: mocha
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
none: false
|
114
|
+
requirements:
|
115
|
+
- - ! '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ! '>='
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
- !ruby/object:Gem::Dependency
|
127
|
+
name: rake
|
128
|
+
requirement: !ruby/object:Gem::Requirement
|
129
|
+
none: false
|
130
|
+
requirements:
|
131
|
+
- - ! '>='
|
132
|
+
- !ruby/object:Gem::Version
|
133
|
+
version: '0'
|
134
|
+
type: :development
|
135
|
+
prerelease: false
|
136
|
+
version_requirements: !ruby/object:Gem::Requirement
|
137
|
+
none: false
|
138
|
+
requirements:
|
139
|
+
- - ! '>='
|
140
|
+
- !ruby/object:Gem::Version
|
141
|
+
version: '0'
|
142
|
+
- !ruby/object:Gem::Dependency
|
143
|
+
name: highline
|
144
|
+
requirement: !ruby/object:Gem::Requirement
|
145
|
+
none: false
|
146
|
+
requirements:
|
147
|
+
- - ! '>='
|
148
|
+
- !ruby/object:Gem::Version
|
149
|
+
version: '0'
|
150
|
+
type: :development
|
151
|
+
prerelease: false
|
152
|
+
version_requirements: !ruby/object:Gem::Requirement
|
153
|
+
none: false
|
154
|
+
requirements:
|
155
|
+
- - ! '>='
|
156
|
+
- !ruby/object:Gem::Version
|
157
|
+
version: '0'
|
158
|
+
description: Provides the executable leap_ca, a deamon that refills a pool of x509
|
159
|
+
client certs stored in CouchDB.
|
160
|
+
email:
|
161
|
+
- azul@leap.se
|
162
|
+
executables:
|
163
|
+
- leap_ca_daemon
|
164
|
+
extensions: []
|
165
|
+
extra_rdoc_files: []
|
166
|
+
files:
|
167
|
+
- config/config_default.yaml
|
168
|
+
- lib/leap_ca/couch_stream.rb
|
169
|
+
- lib/leap_ca/version.rb
|
170
|
+
- lib/leap_ca/config.rb
|
171
|
+
- lib/leap_ca/cert.rb
|
172
|
+
- lib/leap_ca/pool.rb
|
173
|
+
- lib/leap_ca/couch_changes.rb
|
174
|
+
- lib/leap_ca_daemon.rb
|
175
|
+
- lib/leap_ca.rb
|
176
|
+
- bin/leap_ca_daemon
|
177
|
+
- Rakefile
|
178
|
+
- README.md
|
179
|
+
- test/test_helper.rb
|
180
|
+
- test/files/ca.crt
|
181
|
+
- test/files/ca.key
|
182
|
+
- test/unit/couch_stream_test.rb
|
183
|
+
- test/unit/couch_changes_test.rb
|
184
|
+
- test/unit/cert_test.rb
|
185
|
+
- test/config/config.yaml
|
186
|
+
homepage: https://leap.se
|
187
|
+
licenses: []
|
188
|
+
post_install_message:
|
189
|
+
rdoc_options: []
|
190
|
+
require_paths:
|
191
|
+
- lib
|
192
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
193
|
+
none: false
|
194
|
+
requirements:
|
195
|
+
- - ! '>='
|
196
|
+
- !ruby/object:Gem::Version
|
197
|
+
version: '0'
|
198
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
199
|
+
none: false
|
200
|
+
requirements:
|
201
|
+
- - ! '>='
|
202
|
+
- !ruby/object:Gem::Version
|
203
|
+
version: '0'
|
204
|
+
requirements: []
|
205
|
+
rubyforge_project:
|
206
|
+
rubygems_version: 1.8.24
|
207
|
+
signing_key:
|
208
|
+
specification_version: 3
|
209
|
+
summary: Certificate Authority deamon for the LEAP Platform
|
210
|
+
test_files:
|
211
|
+
- test/test_helper.rb
|
212
|
+
- test/files/ca.crt
|
213
|
+
- test/files/ca.key
|
214
|
+
- test/unit/couch_stream_test.rb
|
215
|
+
- test/unit/couch_changes_test.rb
|
216
|
+
- test/unit/cert_test.rb
|
217
|
+
- test/config/config.yaml
|