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,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
+