leap_ca 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|