certmeister 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +20 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +27 -0
- data/LICENSE +20 -0
- data/README.md +32 -0
- data/Rakefile +6 -0
- data/certmeister.gemspec +26 -0
- data/fixtures/ca.crt +15 -0
- data/fixtures/ca.csr +12 -0
- data/fixtures/ca.key +15 -0
- data/fixtures/client.crt +15 -0
- data/fixtures/client.csr +12 -0
- data/fixtures/client.key +15 -0
- data/lib/certmeister.rb +14 -0
- data/lib/certmeister/base.rb +92 -0
- data/lib/certmeister/config.rb +129 -0
- data/lib/certmeister/in_memory_store.rb +43 -0
- data/lib/certmeister/policy.rb +21 -0
- data/lib/certmeister/policy/blackhole.rb +15 -0
- data/lib/certmeister/policy/chain_all.rb +36 -0
- data/lib/certmeister/policy/domain.rb +37 -0
- data/lib/certmeister/policy/existing.rb +32 -0
- data/lib/certmeister/policy/fcrdns.rb +40 -0
- data/lib/certmeister/policy/noop.rb +15 -0
- data/lib/certmeister/policy/psk.rb +37 -0
- data/lib/certmeister/policy/response.rb +24 -0
- data/lib/certmeister/response.rb +47 -0
- data/lib/certmeister/store_error.rb +6 -0
- data/lib/certmeister/test/memory_store_interface.rb +54 -0
- data/lib/certmeister/version.rb +5 -0
- data/signit.rb +39 -0
- data/spec/certmeister/base_spec.rb +205 -0
- data/spec/certmeister/config_spec.rb +170 -0
- data/spec/certmeister/in_memory_store_spec.rb +40 -0
- data/spec/certmeister/policy/blackhole_spec.rb +19 -0
- data/spec/certmeister/policy/chain_all_spec.rb +40 -0
- data/spec/certmeister/policy/domain_spec.rb +38 -0
- data/spec/certmeister/policy/existing_spec.rb +39 -0
- data/spec/certmeister/policy/fcrdns_spec.rb +45 -0
- data/spec/certmeister/policy/noop_spec.rb +17 -0
- data/spec/certmeister/policy/psk_spec.rb +38 -0
- data/spec/certmeister/policy/response_spec.rb +35 -0
- data/spec/certmeister/response_spec.rb +73 -0
- data/spec/helpers/certmeister_config_helper.rb +21 -0
- data/spec/helpers/certmeister_fetching_request_helper.rb +9 -0
- data/spec/helpers/certmeister_policy_helper.rb +14 -0
- data/spec/helpers/certmeister_removing_request_helper.rb +9 -0
- data/spec/helpers/certmeister_signing_request_helper.rb +10 -0
- data/spec/spec_helper.rb +20 -0
- metadata +159 -0
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'certmeister/store_error'
|
2
|
+
|
3
|
+
module Certmeister
|
4
|
+
|
5
|
+
class InMemoryStore
|
6
|
+
|
7
|
+
def initialize(certs = {})
|
8
|
+
@certs = certs
|
9
|
+
@healthy = true
|
10
|
+
end
|
11
|
+
|
12
|
+
def store(cn, cert)
|
13
|
+
fail_if_unhealthy
|
14
|
+
@certs[cn] = cert
|
15
|
+
end
|
16
|
+
|
17
|
+
def fetch(cn)
|
18
|
+
fail_if_unhealthy
|
19
|
+
@certs[cn]
|
20
|
+
end
|
21
|
+
|
22
|
+
def remove(cn)
|
23
|
+
fail_if_unhealthy
|
24
|
+
!!@certs.delete(cn)
|
25
|
+
end
|
26
|
+
|
27
|
+
def health_check
|
28
|
+
@healthy
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def break!
|
34
|
+
@healthy = false
|
35
|
+
end
|
36
|
+
|
37
|
+
def fail_if_unhealthy
|
38
|
+
raise Certmeister::StoreError.new("in-memory store is broken") if !@healthy
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Certmeister
|
2
|
+
|
3
|
+
module Policy
|
4
|
+
|
5
|
+
def self.validate_authenticate_signature(policy)
|
6
|
+
policy and policy.respond_to?(:authenticate) and policy.method(:authenticate).arity == 1
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.validate_authenticate_returns_response(policy)
|
10
|
+
response = policy.authenticate({})
|
11
|
+
response.respond_to?(:authenticated?) and response.respond_to?(:error)
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
Dir.glob(File.join(File.dirname(__FILE__), "policy", "*.rb")) do |path|
|
19
|
+
require path
|
20
|
+
end
|
21
|
+
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'certmeister/policy'
|
2
|
+
|
3
|
+
module Certmeister
|
4
|
+
|
5
|
+
module Policy
|
6
|
+
|
7
|
+
class ChainAll
|
8
|
+
|
9
|
+
def initialize(policys)
|
10
|
+
validate_policys(policys)
|
11
|
+
@policys = policys
|
12
|
+
end
|
13
|
+
|
14
|
+
def authenticate(request)
|
15
|
+
success = Certmeister::Policy::Response.new(true, nil)
|
16
|
+
@policys.inject(success) do |continue, policy|
|
17
|
+
response = policy.authenticate(request)
|
18
|
+
break response unless response.authenticated?
|
19
|
+
continue
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def validate_policys(policys)
|
26
|
+
unless policys.is_a?(Enumerable) and policys.respond_to?(:size) and policys.size > 0 and
|
27
|
+
policys.all? { |policy| Certmeister::Policy.validate_authenticate_signature(policy) }
|
28
|
+
raise ArgumentError.new("enumerable collection of policys required")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'certmeister/policy/response'
|
2
|
+
|
3
|
+
module Certmeister
|
4
|
+
|
5
|
+
module Policy
|
6
|
+
|
7
|
+
class Domain
|
8
|
+
|
9
|
+
def initialize(domains)
|
10
|
+
validate_domains(domains)
|
11
|
+
@domains = domains.map { |domain| ".#{domain}" }
|
12
|
+
end
|
13
|
+
|
14
|
+
def authenticate(request)
|
15
|
+
if not request[:cn]
|
16
|
+
Certmeister::Policy::Response.new(false, "missing cn")
|
17
|
+
elsif not @domains.any? { |domain| request[:cn].end_with?(domain) }
|
18
|
+
Certmeister::Policy::Response.new(false, "cn in unknown domain")
|
19
|
+
else
|
20
|
+
Certmeister::Policy::Response.new(true, nil)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def validate_domains(domains)
|
27
|
+
unless domains.is_a?(Enumerable) and domains.respond_to?(:size) and domains.size > 0 and
|
28
|
+
domains.all? { |psk| psk.respond_to?(:to_s) }
|
29
|
+
raise ArgumentError.new("enumerable collection of domains required")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'certmeister/policy/response'
|
2
|
+
|
3
|
+
module Certmeister
|
4
|
+
|
5
|
+
module Policy
|
6
|
+
|
7
|
+
class Existing
|
8
|
+
|
9
|
+
def initialize(store)
|
10
|
+
is_a_store?(store) or raise ArgumentError.new("expected a fetchable store but received a #{store.class}")
|
11
|
+
@store = store
|
12
|
+
end
|
13
|
+
|
14
|
+
def authenticate(request)
|
15
|
+
if @store.fetch(request[:cn]).nil?
|
16
|
+
Certmeister::Policy::Response.new(true, nil)
|
17
|
+
else
|
18
|
+
Certmeister::Policy::Response.new(false, "certificate for cn already exists")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def is_a_store?(store)
|
25
|
+
store.respond_to?(:fetch)
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'resolv'
|
2
|
+
require 'certmeister/policy/response'
|
3
|
+
|
4
|
+
module Certmeister
|
5
|
+
|
6
|
+
module Policy
|
7
|
+
|
8
|
+
class Fcrdns
|
9
|
+
|
10
|
+
def authenticate(request)
|
11
|
+
begin
|
12
|
+
if not request[:cn]
|
13
|
+
Certmeister::Policy::Response.new(false, "missing cn")
|
14
|
+
elsif not request[:ip]
|
15
|
+
Certmeister::Policy::Response.new(false, "missing ip")
|
16
|
+
elsif not fcrdns_names(request[:ip]).include?(request[:cn])
|
17
|
+
Certmeister::Policy::Response.new(false, "cn in unknown domain")
|
18
|
+
else
|
19
|
+
Certmeister::Policy::Response.new(true, nil)
|
20
|
+
end
|
21
|
+
rescue Resolv::ResolvError => e
|
22
|
+
Certmeister::Policy::Response.new(false, "DNS error (#{e.message})")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def fcrdns_names(ip)
|
29
|
+
resolv = Resolv::DNS.new
|
30
|
+
names = resolv.getnames(ip)
|
31
|
+
addresses = names.inject([]) { |m, name| m.concat(resolv.getaddresses(name)) }
|
32
|
+
reverse_names = addresses.inject([]) { |m, address| m.concat(resolv.getnames(address.to_s)) }
|
33
|
+
(names & reverse_names).map(&:to_s)
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'certmeister/policy/response'
|
2
|
+
|
3
|
+
module Certmeister
|
4
|
+
|
5
|
+
module Policy
|
6
|
+
|
7
|
+
class Psk
|
8
|
+
|
9
|
+
def initialize(psks)
|
10
|
+
validate_psks(psks)
|
11
|
+
@psks = psks.map(&:to_s)
|
12
|
+
end
|
13
|
+
|
14
|
+
def authenticate(request)
|
15
|
+
if not request[:psk]
|
16
|
+
Certmeister::Policy::Response.new(false, "missing psk")
|
17
|
+
elsif not @psks.include?(request[:psk])
|
18
|
+
Certmeister::Policy::Response.new(false, "unknown psk")
|
19
|
+
else
|
20
|
+
Certmeister::Policy::Response.new(true, nil)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def validate_psks(psks)
|
27
|
+
unless psks.is_a?(Enumerable) and psks.respond_to?(:size) and psks.size > 0 and
|
28
|
+
psks.all? { |psk| psk.respond_to?(:to_s) }
|
29
|
+
raise ArgumentError.new("enumerable collection of psks required")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Certmeister
|
2
|
+
|
3
|
+
module Policy
|
4
|
+
|
5
|
+
class Response
|
6
|
+
|
7
|
+
def initialize(authenticated, error)
|
8
|
+
@authenticated = authenticated
|
9
|
+
@error = error
|
10
|
+
end
|
11
|
+
|
12
|
+
def authenticated?
|
13
|
+
!@error
|
14
|
+
end
|
15
|
+
|
16
|
+
def error
|
17
|
+
@error
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Certmeister
|
2
|
+
|
3
|
+
class Response
|
4
|
+
|
5
|
+
def initialize(pem, error)
|
6
|
+
@pem = pem
|
7
|
+
@error = error
|
8
|
+
if @pem and @error
|
9
|
+
raise ArgumentError.new("pem and error are mutually exclusive")
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def pem
|
14
|
+
@pem
|
15
|
+
end
|
16
|
+
|
17
|
+
def error
|
18
|
+
@error
|
19
|
+
end
|
20
|
+
|
21
|
+
def hit?
|
22
|
+
!!@pem
|
23
|
+
end
|
24
|
+
|
25
|
+
def miss?
|
26
|
+
!(hit? or error?)
|
27
|
+
end
|
28
|
+
|
29
|
+
def error?
|
30
|
+
!!@error
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.hit(pem)
|
34
|
+
self.new(pem, nil)
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.miss
|
38
|
+
self.new(nil, nil)
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.error(message)
|
42
|
+
self.new(nil, message)
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Certmeister
|
2
|
+
|
3
|
+
module Test
|
4
|
+
|
5
|
+
module MemoryStoreInterface
|
6
|
+
|
7
|
+
def it_behaves_like_a_certmeister_store
|
8
|
+
|
9
|
+
it "stores certificates by CN (common name)" do
|
10
|
+
pem = File.read('fixtures/client.crt')
|
11
|
+
subject.store('axl.hetzner.africa', pem)
|
12
|
+
expect(subject.fetch('axl.hetzner.africa')).to eql pem
|
13
|
+
end
|
14
|
+
|
15
|
+
it "returns nil when fetching non-existent CN" do
|
16
|
+
expect(subject.fetch('axl.hetzner.africa')).to be_nil
|
17
|
+
end
|
18
|
+
|
19
|
+
it "is not concerned with validating certificates" do
|
20
|
+
expect { subject.store('axl.hetzner.africa', "nonsense") }.to_not raise_error
|
21
|
+
end
|
22
|
+
|
23
|
+
it "overwrites an existing certificate if one exists" do
|
24
|
+
subject.store('axl.hetzner.africa', "first")
|
25
|
+
subject.store('axl.hetzner.africa', "second")
|
26
|
+
expect(subject.fetch('axl.hetzner.africa')).to eql "second"
|
27
|
+
end
|
28
|
+
|
29
|
+
it "deletes certificates by CN (common name)" do
|
30
|
+
subject.store('axl.hetzner.africa', "cert")
|
31
|
+
expect(subject.remove('axl.hetzner.africa')).to be_true
|
32
|
+
expect(subject.fetch('axl.hetzner.africa')).to be_nil
|
33
|
+
end
|
34
|
+
|
35
|
+
it "returns false when removing a non-existent CN" do
|
36
|
+
expect(subject.remove('axl.hetzner.africa')).to be_false
|
37
|
+
end
|
38
|
+
|
39
|
+
it "returns true from health_check when healthy" do
|
40
|
+
expect(subject.health_check).to be_true
|
41
|
+
end
|
42
|
+
|
43
|
+
it "returns false from health_check when not healthy" do
|
44
|
+
subject.send(:break!)
|
45
|
+
expect(subject.health_check).to be_false
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
data/signit.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# Inspired by https://gist.github.com/mitfik/1922961
|
2
|
+
|
3
|
+
require 'openssl'
|
4
|
+
require 'time'
|
5
|
+
|
6
|
+
if OpenSSL::Digest.const_defined?('SHA256')
|
7
|
+
@digest = OpenSSL::Digest::SHA256
|
8
|
+
elsif OpenSSL::Digest.const_defined?('SHA1')
|
9
|
+
@digest = OpenSSL::Digest::SHA1
|
10
|
+
else
|
11
|
+
raise "No FIPS 140-2 compliant digest algorithm in OpenSSL::Digest"
|
12
|
+
end
|
13
|
+
|
14
|
+
ca_cert_data = File.read('fixtures/ca.crt')
|
15
|
+
ca_key_data = File.read('fixtures/ca.key')
|
16
|
+
|
17
|
+
ca_cert = OpenSSL::X509::Certificate.new(ca_cert_data)
|
18
|
+
ca_key = OpenSSL::PKey.read(ca_key_data)
|
19
|
+
puts "# CA cert"
|
20
|
+
puts ca_cert.to_pem
|
21
|
+
|
22
|
+
csr_data = File.read('fixtures/client.csr')
|
23
|
+
csr = OpenSSL::X509::Request.new(csr_data)
|
24
|
+
puts "# client certificate signing request"
|
25
|
+
puts csr.to_pem
|
26
|
+
|
27
|
+
now = DateTime.now
|
28
|
+
cert = OpenSSL::X509::Certificate.new
|
29
|
+
cert.serial = 0
|
30
|
+
cert.version = 2
|
31
|
+
cert.not_before = now.to_time
|
32
|
+
cert.not_after = (now + (5 * 365)).to_time
|
33
|
+
cert.subject = csr.subject
|
34
|
+
cert.public_key = csr.public_key
|
35
|
+
cert.issuer = ca_cert.subject
|
36
|
+
cert.sign ca_key, @digest.new
|
37
|
+
|
38
|
+
puts "# client certificate"
|
39
|
+
puts cert.to_pem
|