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 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
@@ -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: "_"
@@ -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
@@ -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
@@ -0,0 +1,4 @@
1
+ module LeapCA
2
+ VERSION = "0.2.0"
3
+ REQUIRE_PATHS = ['lib']
4
+ 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-----
@@ -0,0 +1,10 @@
1
+ require 'rubygems'
2
+ require 'minitest/autorun'
3
+
4
+ BASE_DIR = File.expand_path('../..', __FILE__)
5
+ $:.unshift File.expand_path('lib', BASE_DIR)
6
+
7
+ require 'mocha/setup'
8
+
9
+ LEAP_CA_CONFIG = "test/config/config.yaml"
10
+ require 'leap_ca'
@@ -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