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