certmeister 0.0.1
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.
- 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
|