certmeister 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +20 -0
  3. data/.rspec +2 -0
  4. data/.ruby-gemset +1 -0
  5. data/.ruby-version +1 -0
  6. data/Gemfile +4 -0
  7. data/Gemfile.lock +27 -0
  8. data/LICENSE +20 -0
  9. data/README.md +32 -0
  10. data/Rakefile +6 -0
  11. data/certmeister.gemspec +26 -0
  12. data/fixtures/ca.crt +15 -0
  13. data/fixtures/ca.csr +12 -0
  14. data/fixtures/ca.key +15 -0
  15. data/fixtures/client.crt +15 -0
  16. data/fixtures/client.csr +12 -0
  17. data/fixtures/client.key +15 -0
  18. data/lib/certmeister.rb +14 -0
  19. data/lib/certmeister/base.rb +92 -0
  20. data/lib/certmeister/config.rb +129 -0
  21. data/lib/certmeister/in_memory_store.rb +43 -0
  22. data/lib/certmeister/policy.rb +21 -0
  23. data/lib/certmeister/policy/blackhole.rb +15 -0
  24. data/lib/certmeister/policy/chain_all.rb +36 -0
  25. data/lib/certmeister/policy/domain.rb +37 -0
  26. data/lib/certmeister/policy/existing.rb +32 -0
  27. data/lib/certmeister/policy/fcrdns.rb +40 -0
  28. data/lib/certmeister/policy/noop.rb +15 -0
  29. data/lib/certmeister/policy/psk.rb +37 -0
  30. data/lib/certmeister/policy/response.rb +24 -0
  31. data/lib/certmeister/response.rb +47 -0
  32. data/lib/certmeister/store_error.rb +6 -0
  33. data/lib/certmeister/test/memory_store_interface.rb +54 -0
  34. data/lib/certmeister/version.rb +5 -0
  35. data/signit.rb +39 -0
  36. data/spec/certmeister/base_spec.rb +205 -0
  37. data/spec/certmeister/config_spec.rb +170 -0
  38. data/spec/certmeister/in_memory_store_spec.rb +40 -0
  39. data/spec/certmeister/policy/blackhole_spec.rb +19 -0
  40. data/spec/certmeister/policy/chain_all_spec.rb +40 -0
  41. data/spec/certmeister/policy/domain_spec.rb +38 -0
  42. data/spec/certmeister/policy/existing_spec.rb +39 -0
  43. data/spec/certmeister/policy/fcrdns_spec.rb +45 -0
  44. data/spec/certmeister/policy/noop_spec.rb +17 -0
  45. data/spec/certmeister/policy/psk_spec.rb +38 -0
  46. data/spec/certmeister/policy/response_spec.rb +35 -0
  47. data/spec/certmeister/response_spec.rb +73 -0
  48. data/spec/helpers/certmeister_config_helper.rb +21 -0
  49. data/spec/helpers/certmeister_fetching_request_helper.rb +9 -0
  50. data/spec/helpers/certmeister_policy_helper.rb +14 -0
  51. data/spec/helpers/certmeister_removing_request_helper.rb +9 -0
  52. data/spec/helpers/certmeister_signing_request_helper.rb +10 -0
  53. data/spec/spec_helper.rb +20 -0
  54. 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,15 @@
1
+ require 'certmeister/policy/response'
2
+
3
+ module Certmeister
4
+
5
+ module Policy
6
+
7
+ class Blackhole
8
+ def authenticate(request)
9
+ Certmeister::Policy::Response.new(false, "blackholed")
10
+ end
11
+ end
12
+
13
+ end
14
+
15
+ end
@@ -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,15 @@
1
+ require 'certmeister/policy/response'
2
+
3
+ module Certmeister
4
+
5
+ module Policy
6
+
7
+ class Noop
8
+ def authenticate(request)
9
+ Certmeister::Policy::Response.new(true, nil)
10
+ end
11
+ end
12
+
13
+ end
14
+
15
+ 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,6 @@
1
+ module Certmeister
2
+
3
+ class StoreError < StandardError
4
+ end
5
+
6
+ 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
@@ -0,0 +1,5 @@
1
+ module Certmeister
2
+
3
+ VERSION = '0.0.1' unless defined?(VERSION)
4
+
5
+ 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