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,205 @@
1
+ require 'spec_helper'
2
+ require 'helpers/certmeister_config_helper'
3
+ require 'helpers/certmeister_signing_request_helper'
4
+
5
+ require 'certmeister'
6
+ require 'openssl'
7
+
8
+ describe Certmeister do
9
+
10
+ it "is configured at instantiation" do
11
+ expect { Certmeister.new(CertmeisterConfigHelper::valid_config) }.to_not raise_error
12
+ end
13
+
14
+ it "cannot be instantiated with an invalid config" do
15
+ expect { Certmeister.new(Certmeister::Config.new({})) }.to raise_error(RuntimeError, /invalid config/)
16
+ end
17
+
18
+ describe "#sign(request)" do
19
+
20
+ let(:valid_request) { CertmeisterSigningRequestHelper::valid_request }
21
+
22
+ describe "refuses" do
23
+
24
+ it "refuses the request if it has no cn" do
25
+ ca = Certmeister.new(CertmeisterConfigHelper::valid_config)
26
+ response = ca.sign({})
27
+ invalid_request = valid_request.tap { |o| o.delete(:cn) }
28
+ response = ca.sign(invalid_request)
29
+ expect(response.error).to match /CN/
30
+ end
31
+
32
+ it "refuses the request if the sign policy declines it" do
33
+ options = CertmeisterConfigHelper::valid_config_options
34
+ options[:sign_policy] = Certmeister::Policy::Blackhole.new
35
+ ca = Certmeister.new(Certmeister::Config.new(options))
36
+ response = ca.sign(valid_request)
37
+ expect(response.error).to eql "request refused (blackholed)"
38
+ end
39
+
40
+ it "refuses to sign an invalid CSR" do
41
+ ca = Certmeister.new(CertmeisterConfigHelper::valid_config)
42
+ invalid_request = valid_request.tap { |o| o[:csr] = "a terrible misunderstanding" }
43
+ response = ca.sign(invalid_request)
44
+ expect(response.error).to eql "invalid CSR (not enough data)"
45
+ end
46
+
47
+ it "refuses to sign a CSR if the subject does not agree with the request CN" do
48
+ request = valid_request.tap { |r| r[:cn] = "monkeyface.example.com" }
49
+ ca = Certmeister.new(CertmeisterConfigHelper::valid_config)
50
+ response = ca.sign(request)
51
+ expect(response.error).to eql "CSR subject (axl.hetzner.africa) disagrees with request CN (monkeyface.example.com)"
52
+ end
53
+
54
+ end
55
+
56
+ describe "signing" do
57
+
58
+ def sign_valid_request
59
+ ca = Certmeister.new(CertmeisterConfigHelper::valid_config)
60
+ ca.sign(valid_request)
61
+ end
62
+
63
+ it "signs a CSR if the sign policy passes the request" do
64
+ response = sign_valid_request
65
+ expect(response).to be_hit
66
+ end
67
+
68
+ it "sets the issuer to the subject of the CA certificate" do
69
+ response = sign_valid_request
70
+ cert = OpenSSL::X509::Certificate.new(response.pem)
71
+ expect(cert.issuer.to_s).to match /CN=Certmeister Test CA/
72
+ end
73
+
74
+ it "sets the subject to the subject of the CSR" do
75
+ response = sign_valid_request
76
+ cert = OpenSSL::X509::Certificate.new(response.pem)
77
+ expect(cert.subject.to_s).to match /CN=axl.hetzner.africa/
78
+ end
79
+
80
+ it "sets validity to 5 years from now" do
81
+ now = (DateTime.now.to_time - 1)
82
+ response = sign_valid_request
83
+ cert = OpenSSL::X509::Certificate.new(response.pem)
84
+ expect(cert.not_before).to be >= now
85
+ expect(cert.not_after - cert.not_before).to be < (5 * 365 * 24 * 60 * 60 + 2)
86
+ expect(cert.not_after - cert.not_before).to be >= (5 * 365 * 24 * 60 * 60)
87
+ end
88
+
89
+ it "stores the signed certificate, indexed on request CN" do
90
+ config = CertmeisterConfigHelper::valid_config
91
+ ca = Certmeister.new(config)
92
+ response = ca.sign(valid_request)
93
+ stored = config.store.fetch('axl.hetzner.africa')
94
+ expect(stored).to eql response.pem
95
+ end
96
+
97
+ it "does not capture errors from the store" do
98
+ config = CertmeisterConfigHelper::valid_config
99
+ config.store.send(:break!)
100
+ ca = Certmeister.new(config)
101
+ expect { ca.sign(valid_request) }.to raise_error(Certmeister::StoreError)
102
+ end
103
+
104
+ end
105
+
106
+ end
107
+
108
+ describe "#fetch(request)" do
109
+
110
+ describe "refuses" do
111
+
112
+ it "refuses the request if it has no cn" do
113
+ ca = Certmeister.new(CertmeisterConfigHelper::valid_config)
114
+ response = ca.fetch({})
115
+ expect(response.error).to match /CN/
116
+ end
117
+
118
+ it "refuses the request if the fetch policy declines it" do
119
+ options = CertmeisterConfigHelper::valid_config_options
120
+ options[:fetch_policy] = Certmeister::Policy::Blackhole.new
121
+ ca = Certmeister.new(Certmeister::Config.new(options))
122
+ response = ca.fetch(cn: 'axl.starjuice.net')
123
+ expect(response.error).to eql "request refused (blackholed)"
124
+ end
125
+
126
+ end
127
+
128
+ it "returns a miss if the store has no certificate for the cn" do
129
+ ca = Certmeister.new(CertmeisterConfigHelper::valid_config)
130
+ expect(ca.fetch(cn: 'axl.starjuice.net')).to be_miss
131
+ end
132
+
133
+ it "returns the certificate as a PEM-encoded string when the store has a certificate for the cn" do
134
+ config = CertmeisterConfigHelper::valid_config
135
+ config.store.store('axl.starjuice.net', '...')
136
+ ca = Certmeister.new(config)
137
+ expect(ca.fetch(cn: 'axl.starjuice.net').pem).to eql '...'
138
+ end
139
+
140
+ class StoreWithBrokenFetch
141
+ def store(cn, cert); end
142
+ def fetch(cn); raise Certmeister::StoreError.new("simulated error"); end
143
+ def health_check; end
144
+ end
145
+
146
+ it "does not capture errors from the store" do
147
+ config = CertmeisterConfigHelper::valid_config
148
+ config.store.send(:break!)
149
+ ca = Certmeister.new(config)
150
+ expect { ca.fetch(cn: 'axl.starjuice.net') }.to raise_error(Certmeister::StoreError)
151
+ end
152
+
153
+ end
154
+
155
+ describe "#remove(cn)" do
156
+
157
+ describe "refuses" do
158
+
159
+ it "refuses the request if it has no cn" do
160
+ ca = Certmeister.new(CertmeisterConfigHelper::valid_config)
161
+ response = ca.remove({})
162
+ expect(response.error).to match /CN/
163
+ end
164
+
165
+ it "refuses the request if the fetch policy declines it" do
166
+ options = CertmeisterConfigHelper::valid_config_options
167
+ options[:remove_policy] = Certmeister::Policy::Blackhole.new
168
+ ca = Certmeister.new(Certmeister::Config.new(options))
169
+ response = ca.remove(cn: 'axl.starjuice.net')
170
+ expect(response.error).to eql "request refused (blackholed)"
171
+ end
172
+
173
+ end
174
+
175
+ it "returns a hit if the certificate existed in the store" do
176
+ config = CertmeisterConfigHelper::valid_config
177
+ config.store.store('axl.starjuice.net', '...')
178
+ ca = Certmeister.new(config)
179
+ expect(ca.remove(cn: 'axl.starjuice.net')).to be_hit
180
+ end
181
+
182
+ it "returns a miss if the certificate did not exist in the store" do
183
+ ca = Certmeister.new(CertmeisterConfigHelper::valid_config)
184
+ expect(ca.remove(cn: 'axl.starjuice.net')).to be_miss
185
+ end
186
+
187
+ it "removes the certificate from the store" do
188
+ config = CertmeisterConfigHelper::valid_config
189
+ config.store.store('axl.starjuice.net', '...')
190
+ ca = Certmeister.new(config)
191
+ ca.remove(cn: 'axl.starjuice.net')
192
+ expect(config.store.fetch('axl.starjuice.net')).to be_nil
193
+ end
194
+
195
+ it "does not capture errors from the store" do
196
+ config = CertmeisterConfigHelper::valid_config
197
+ config.store.send(:break!)
198
+ ca = Certmeister.new(config)
199
+ expect { ca.remove(cn: 'axl.starjuice.net') }.to raise_error(Certmeister::StoreError)
200
+ end
201
+
202
+ end
203
+
204
+ end
205
+
@@ -0,0 +1,170 @@
1
+ require 'spec_helper'
2
+ require 'helpers/certmeister_config_helper'
3
+ require 'helpers/certmeister_policy_helper'
4
+
5
+ require 'certmeister'
6
+
7
+ describe Certmeister::Config do
8
+
9
+ let(:options) { CertmeisterConfigHelper::valid_config_options }
10
+
11
+ def config_option_is_required(option)
12
+ options.delete(option)
13
+ config = Certmeister::Config.new(options)
14
+ expect(config).to_not be_valid
15
+ expect(config.errors[option]).to eql "is required"
16
+ end
17
+
18
+ def config_option_provides_method_with_arity(option, method, arity)
19
+ arity_name = case arity
20
+ when 0 then "nullary"
21
+ when 1 then "unary"
22
+ when 2 then "binary"
23
+ when 3 then "ternary"
24
+ else
25
+ raise "broken test helper does not support arity #{4}"
26
+ end
27
+ options[option].send(:define_singleton_method, method) { |wrong, number, of, arguments| }
28
+
29
+ config = Certmeister::Config.new(options)
30
+ expect(config).to_not be_valid
31
+ expect(config.errors[option]).to eql "must provide a #{arity_name} #{method} method"
32
+ end
33
+
34
+ it "does not allow unknown options" do
35
+ options[:unknown] = 1
36
+ config = Certmeister::Config.new(options)
37
+ expect(config).to_not be_valid
38
+ expect(config.errors[:unknown]).to eql "is not a supported option"
39
+ end
40
+
41
+ describe ":ca_cert" do
42
+
43
+ it "is required" do
44
+ config_option_is_required(:ca_cert)
45
+ end
46
+
47
+ it "must be a PEM-encoded x509 certificate" do
48
+ options[:ca_cert] = "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----\n"
49
+ config = Certmeister::Config.new(options)
50
+ expect(config).to_not be_valid
51
+ expect(config.errors[:ca_cert]).to eql "must be a PEM-encoded x509 certificate (nested asn1 error)"
52
+ end
53
+
54
+ it "is accessible as an OpenSSL::X509::Certificate object" do
55
+ config = Certmeister::Config.new(options)
56
+ expect(config.ca_cert).to be_a(OpenSSL::X509::Certificate)
57
+ end
58
+
59
+ end
60
+
61
+ describe ":ca_key" do
62
+
63
+ it "is required" do
64
+ config_option_is_required(:ca_key)
65
+ end
66
+
67
+ it "must be a string containing an x509 certificate in PEM encoding" do
68
+ options[:ca_key] = "-----BEGIN RSA PRIVATE KEY-----\n-----END RSA PRIVATE KEY-----\n"
69
+ config = Certmeister::Config.new(options)
70
+ expect(config).to_not be_valid
71
+ expect(config.errors[:ca_key]).to eql "must be a PEM-encoded private key (Could not parse PKey)"
72
+ end
73
+
74
+ it "is accessible as an OpenSSL::PKey::PKey object" do
75
+ config = Certmeister::Config.new(options)
76
+ expect(config.ca_key).to be_a(OpenSSL::PKey::PKey)
77
+ end
78
+
79
+ end
80
+
81
+ describe ":store" do
82
+
83
+ it "is required" do
84
+ config_option_is_required(:store)
85
+ end
86
+
87
+ it "must provide a binary store method" do
88
+ config_option_provides_method_with_arity(:store, :store, 2)
89
+ end
90
+
91
+ it "must provide a unary fetch method" do
92
+ config_option_provides_method_with_arity(:store, :fetch, 1)
93
+ end
94
+
95
+ it "must provide a unary remove method" do
96
+ config_option_provides_method_with_arity(:store, :remove, 1)
97
+ end
98
+
99
+ it "must provide a nullary health_check method" do
100
+ config_option_provides_method_with_arity(:store, :health_check, 0)
101
+ end
102
+
103
+ it "is accessible" do
104
+ config = Certmeister::Config.new(options)
105
+ expect(config.store).to eql options[:store]
106
+ end
107
+
108
+ end
109
+
110
+ [:sign_policy, :fetch_policy, :remove_policy].each do |policy|
111
+ describe ":#{policy}" do
112
+
113
+ it "is required" do
114
+ config_option_is_required(policy)
115
+ end
116
+
117
+ it "must provide a unary authenticate method" do
118
+ config_option_provides_method_with_arity(policy, :authenticate, 1)
119
+ end
120
+
121
+ it "must return a Certmeister::Policy::Response from the authenticate method" do
122
+ options[policy] = CertmeisterPolicyHelper::BrokenPolicy.new
123
+ config = Certmeister::Config.new(options)
124
+ expect(config).to_not be_valid
125
+ expect(config.errors[policy]).to eql "must return a policy response"
126
+ end
127
+
128
+ it "is accessible" do
129
+ config = Certmeister::Config.new(options)
130
+ expect(config.send(policy)).to eql options[policy]
131
+ end
132
+
133
+ end
134
+ end
135
+
136
+ describe "error_list" do
137
+
138
+ it "is empty if the config has no errors" do
139
+ config = Certmeister::Config.new(options)
140
+ config.valid?
141
+ expect(config.error_list).to be_empty
142
+ end
143
+
144
+ it "includes one string (option and message) per error if the config has errors" do
145
+ options.delete(:ca_cert)
146
+ options.delete(:ca_key)
147
+ config = Certmeister::Config.new(options)
148
+ config.valid?
149
+ expect(config.error_list).to match_array ["ca_cert is required", "ca_key is required"]
150
+ end
151
+
152
+ end
153
+
154
+ describe "openssl_digest" do
155
+
156
+ it "causes a validation failure if the OpenSSL library doesn't provide one" do
157
+ expect(OpenSSL::Digest).to receive(:const_defined?).twice.and_return(nil)
158
+ config = Certmeister::Config.new(options)
159
+ expect(config).to_not be_valid
160
+ expect(config.errors[:openssl_digest]).to eql "can't find FIPS 140-2 compliant algorithm in OpenSSL::Digest"
161
+ end
162
+
163
+ it "is accessible without being supplied" do
164
+ config = Certmeister::Config.new(options)
165
+ expect([OpenSSL::Digest::SHA256, OpenSSL::Digest::SHA1]).to include(config.openssl_digest)
166
+ end
167
+
168
+ end
169
+
170
+ end
@@ -0,0 +1,40 @@
1
+ require 'spec_helper'
2
+ require 'certmeister/test/memory_store_interface'
3
+
4
+ require 'certmeister/in_memory_store'
5
+
6
+ describe Certmeister::InMemoryStore do
7
+
8
+ class << self
9
+ include Certmeister::Test::MemoryStoreInterface
10
+ end
11
+
12
+ it_behaves_like_a_certmeister_store
13
+
14
+ describe "for use in testing" do
15
+
16
+ it "can be initialized with an existing data set" do
17
+ existing = {'axl.hetzner.africa' => '...cert...'}
18
+ store = Certmeister::InMemoryStore.new(existing)
19
+ expect(store.fetch('axl.hetzner.africa')).to eql '...cert...'
20
+ end
21
+
22
+ it "store raises Certmeister::StoreError when broken" do
23
+ subject.send(:break!)
24
+ expect { subject.store('axl.hetzner.africa', "first") }.to raise_error(Certmeister::StoreError)
25
+ end
26
+
27
+ it "fetch raises Certmeister::StoreError when broken" do
28
+ subject.send(:break!)
29
+ expect { subject.fetch('axl.hetzner.africa') }.to raise_error(Certmeister::StoreError, "in-memory store is broken")
30
+ end
31
+
32
+ it "remove raises Certmeister::StoreError when broken" do
33
+ subject.send(:break!)
34
+ expect { subject.remove('axl.hetzner.africa') }.to raise_error(Certmeister::StoreError, "in-memory store is broken")
35
+ end
36
+
37
+ end
38
+
39
+ end
40
+
@@ -0,0 +1,19 @@
1
+ require 'spec_helper'
2
+ require 'helpers/certmeister_signing_request_helper'
3
+
4
+ require 'certmeister/policy/blackhole'
5
+
6
+ describe Certmeister::Policy::Blackhole do
7
+
8
+ it "demands a request" do
9
+ expect { subject.authenticate }.to raise_error(ArgumentError)
10
+ end
11
+
12
+ it "refuses any request" do
13
+ response = subject.authenticate(CertmeisterSigningRequestHelper::valid_request)
14
+ expect(response).to_not be_authenticated
15
+ expect(response.error).to eql "blackholed"
16
+ end
17
+
18
+ end
19
+
@@ -0,0 +1,40 @@
1
+ require 'spec_helper'
2
+ require 'certmeister/policy/blackhole'
3
+ require 'certmeister/policy/noop'
4
+
5
+ require 'certmeister/policy/chain_all'
6
+
7
+ describe Certmeister::Policy::ChainAll do
8
+
9
+ it "must be configured with a list of policys" do
10
+ expected_error = "enumerable collection of policys required"
11
+ expect { Certmeister::Policy::ChainAll.new }.to raise_error(ArgumentError)
12
+ expect { Certmeister::Policy::ChainAll.new(Certmeister::Policy::Noop.new) }.to raise_error(ArgumentError, expected_error)
13
+ expect { Certmeister::Policy::ChainAll.new([]) }.to raise_error(ArgumentError, expected_error)
14
+ end
15
+
16
+ it "demands a request" do
17
+ policy = Certmeister::Policy::ChainAll.new([Certmeister::Policy::Noop.new])
18
+ expect { policy.authenticate }.to raise_error(ArgumentError)
19
+ end
20
+
21
+ it "authenticates a request that all its chained policys authenticate" do
22
+ policy = Certmeister::Policy::ChainAll.new([Certmeister::Policy::Noop.new, Certmeister::Policy::Noop.new])
23
+ response = policy.authenticate({anything: 'something'})
24
+ expect(response).to be_authenticated
25
+ end
26
+
27
+ it "refuses a request that any one of its chained policys refuses" do
28
+ refuse_last = Certmeister::Policy::ChainAll.new([ Certmeister::Policy::Noop.new, Certmeister::Policy::Blackhole.new])
29
+ refuse_first = Certmeister::Policy::ChainAll.new([ Certmeister::Policy::Blackhole.new, Certmeister::Policy::Noop.new])
30
+ policys = [refuse_last, refuse_first]
31
+
32
+ policys.each do |policy|
33
+ response = policy.authenticate({anything: 'something'})
34
+ expect(response).to_not be_authenticated
35
+ expect(response.error).to eql "blackholed"
36
+ end
37
+ end
38
+
39
+ end
40
+