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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 546ac60693e496000a1fcb1c52cfe9324e091efe
4
+ data.tar.gz: ecfe80b2211241e9782329a705ea9e8e9869d51b
5
+ SHA512:
6
+ metadata.gz: 3a7528b98e7f6c92527b53f8d0038535ae49c7b62265d97b35964f6c93d9dbda60e7c0dc796ae5b85834f552f0e5ede845314ed412ca32bb50c6abcfebab4210
7
+ data.tar.gz: 1ddc2e5d38a52438fcb9a1420077e19117edb6c1d08126cbde580c656fb93b946c0135cdcff085d4e82ae5a7ba13e5abed57b08e8325dd750c2a3a4b28571ddb
data/.gitignore ADDED
@@ -0,0 +1,20 @@
1
+ *~
2
+ *.sw*
3
+ *.gem
4
+ *.rbc
5
+ .bundle
6
+ .config
7
+ coverage
8
+ InstalledFiles
9
+ lib/bundler/man
10
+ pkg
11
+ rdoc
12
+ spec/reports
13
+ test/tmp
14
+ test/version_tmp
15
+ tmp
16
+
17
+ # YARD artifacts
18
+ .yardoc
19
+ _yardoc
20
+ doc/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/.ruby-gemset ADDED
@@ -0,0 +1 @@
1
+ certmeister
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-2.0.0-p247
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'http://rubygems.org'
2
+
3
+ gemspec
4
+
data/Gemfile.lock ADDED
@@ -0,0 +1,27 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ certmeister (0.0.1)
5
+
6
+ GEM
7
+ remote: http://rubygems.org/
8
+ specs:
9
+ diff-lcs (1.2.5)
10
+ rake (0.9.6)
11
+ rspec (2.14.1)
12
+ rspec-core (~> 2.14.0)
13
+ rspec-expectations (~> 2.14.0)
14
+ rspec-mocks (~> 2.14.0)
15
+ rspec-core (2.14.7)
16
+ rspec-expectations (2.14.4)
17
+ diff-lcs (>= 1.1.3, < 2.0)
18
+ rspec-mocks (2.14.4)
19
+
20
+ PLATFORMS
21
+ ruby
22
+
23
+ DEPENDENCIES
24
+ bundler (~> 1.5)
25
+ certmeister!
26
+ rake (~> 0)
27
+ rspec (~> 2.14)
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2013 Sheldon Hearn
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,32 @@
1
+ # Certmeister
2
+
3
+ Certmeister is a conditionally autosigning Certificate Authority. It was developed for use
4
+ with the Puppet infrastructure at Hetzner PTY Ltd.
5
+
6
+ The service will autosign a certificate request when the configurable access policy permits.
7
+ The reference access policy in use by Hetzner PTY Ltd is:
8
+
9
+ * the Common Name (CN) of the certificate is in the host-h.net domain,
10
+ * the service has no record of already having signed a certificate for that CN, and
11
+ * the requesting client IP address has forward confirmed reverse DNS that matches the CN.
12
+ * Requests to fetch certificates are always allowed.
13
+ * Requests to delete certificates are only allowed when they originate from
14
+ a secure operator network.
15
+
16
+ This allows us the convenience of Puppet's autosign feature, without the horrendous security implications.
17
+
18
+ Certmeister is the core of a fancy web service that does this:
19
+
20
+ ```
21
+ cat request/client.csr | openssl x509 -req -CA CA/ca.crt -CAkey CA/ca.key -CAcreateserial -addtrust clientAuth > CA/signed/<cn>.crt
22
+ ```
23
+
24
+ To hit the service:
25
+
26
+ ```
27
+ $ curl -L \
28
+ -d "psk=secretkey" \
29
+ -d "csr=$(perl -MURI::Escape -e 'print uri_escape(join("", <STDIN>));' < request/client.csr)" \
30
+ http://certmeister.hetzner.co.za/certificate/$(hostname --fqdn) > request/client.crt
31
+ ```
32
+
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'certmeister/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "certmeister"
8
+ spec.version = Certmeister::VERSION
9
+ spec.authors = ["Sheldon Hearn"]
10
+ spec.email = ["sheldonh@starjuice.net"]
11
+ spec.summary = %q{Conditionally autosigning certificate authority.}
12
+ spec.description = %q{Certificate authority that can be configured to make decisions about whether to autosign certificate signing requests for clients. This gem provides the protocol-agnostic library, which is expected to be used within something like an HTTP REST service.}
13
+ spec.homepage = "https://github.com/sheldonh/certmeister"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.5"
22
+ spec.add_development_dependency "rake", "~> 0"
23
+ spec.add_development_dependency "rspec", "~> 2.14"
24
+ end
25
+
26
+
data/fixtures/ca.crt ADDED
@@ -0,0 +1,15 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ MIICVzCCAcACCQDZ9VpcDtwY1TANBgkqhkiG9w0BAQUFADBwMQswCQYDVQQGEwJa
3
+ QTEVMBMGA1UECAwMV2VzdGVybiBDYXBlMRIwEAYDVQQHDAlDYXBlIFRvd24xGDAW
4
+ BgNVBAoMD0hldHpuZXIgUFRZIEx0ZDEcMBoGA1UEAwwTQ2VydG1laXN0ZXIgVGVz
5
+ dCBDQTAeFw0xMzEyMTgwOTQxMTdaFw0xNjEyMTcwOTQxMTdaMHAxCzAJBgNVBAYT
6
+ AlpBMRUwEwYDVQQIDAxXZXN0ZXJuIENhcGUxEjAQBgNVBAcMCUNhcGUgVG93bjEY
7
+ MBYGA1UECgwPSGV0em5lciBQVFkgTHRkMRwwGgYDVQQDDBNDZXJ0bWVpc3RlciBU
8
+ ZXN0IENBMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDE+JkNX7kXFGZ2JyFH
9
+ GfMgXhzazKoGJj/HV3aJSqTjRVrYkgTgCz1aIGj/DXd3gTbturC4s4SiU0i1Gi5K
10
+ J8dHVfDlz/jNbPG4LiMOL1B0TQV4MyVdKJFn6nZCbBqvhCc0vHGw1O7xFiuRBmcP
11
+ SJyqT7Qwz8B1hYNQggVLbPAwawIDAQABMA0GCSqGSIb3DQEBBQUAA4GBADptm7G+
12
+ 9jt9v9yjReQu8KgPcMOfzvNBH8g6s4WH7fn7FAj7//92YsYzX2Ost2zPzdsYCSgD
13
+ DucgmG/cezUCyoljqmND5gfcEtk2WjdXF/Sd+Ulnr02L0QTmIzHNf52rRfZGH8O1
14
+ dfx9ZA9mS0uPXd6ePgJI5/y7x7tgHjsm1VOz
15
+ -----END CERTIFICATE-----
data/fixtures/ca.csr ADDED
@@ -0,0 +1,12 @@
1
+ -----BEGIN CERTIFICATE REQUEST-----
2
+ MIIBsDCCARkCAQAwcDELMAkGA1UEBhMCWkExFTATBgNVBAgMDFdlc3Rlcm4gQ2Fw
3
+ ZTESMBAGA1UEBwwJQ2FwZSBUb3duMRgwFgYDVQQKDA9IZXR6bmVyIFBUWSBMdGQx
4
+ HDAaBgNVBAMME0NlcnRtZWlzdGVyIFRlc3QgQ0EwgZ8wDQYJKoZIhvcNAQEBBQAD
5
+ gY0AMIGJAoGBAMT4mQ1fuRcUZnYnIUcZ8yBeHNrMqgYmP8dXdolKpONFWtiSBOAL
6
+ PVogaP8Nd3eBNu26sLizhKJTSLUaLkonx0dV8OXP+M1s8bguIw4vUHRNBXgzJV0o
7
+ kWfqdkJsGq+EJzS8cbDU7vEWK5EGZw9InKpPtDDPwHWFg1CCBUts8DBrAgMBAAGg
8
+ ADANBgkqhkiG9w0BAQUFAAOBgQAJL3fCo4b9Y05+Sims2j58dX599cKzjWJFxEFr
9
+ +nNleWoxRu8yEzKTGW9F8xrBHIM3lN8ULLEWr4B0kG+k1vMc2ZQHaalRL3OE386k
10
+ Swt5HcL01He6cLnQppIlDNhj5LpkGRipzQZrXip2NCQTYEEKX6PEed/hqv20jXU1
11
+ ge75bA==
12
+ -----END CERTIFICATE REQUEST-----
data/fixtures/ca.key ADDED
@@ -0,0 +1,15 @@
1
+ -----BEGIN RSA PRIVATE KEY-----
2
+ MIICXAIBAAKBgQDE+JkNX7kXFGZ2JyFHGfMgXhzazKoGJj/HV3aJSqTjRVrYkgTg
3
+ Cz1aIGj/DXd3gTbturC4s4SiU0i1Gi5KJ8dHVfDlz/jNbPG4LiMOL1B0TQV4MyVd
4
+ KJFn6nZCbBqvhCc0vHGw1O7xFiuRBmcPSJyqT7Qwz8B1hYNQggVLbPAwawIDAQAB
5
+ AoGAQf6hGTAHTcpSAiheJ/pz0VZ3CIAmP2U1XU7asmlGEbe9Fm7mH0LkzXuqcjpK
6
+ 2sl6Y/B3IYtUVybcZ4FcHRBy3ccOZtV7mkZtJqg8Uso9pHKvtgvAleeGB8PRQlfr
7
+ Ah9hv+vGKqbfrh3gFupzJGkw0oYxknW0Cf/adcoc+FsfgDECQQDveKbW7lUajYkz
8
+ Gvx0N5VbzemQz01LO8AtYfnV7875lIDaBBxuT5GyOVKq2zCT6t7oOtGdyY520P7J
9
+ WPu6UvONAkEA0pD8ovjMYVIxe5FrTyIeqhj6zx8Ji1oYA0x/4aEwNjZ3wT8gdJXw
10
+ 3zLkoGbJFj1OD+qvn/IyKdsU+Sjjzsh51wJALZVe1MzbQEmu0x6Q7aJi+O2yRxFe
11
+ 2jJOe5UJ1JJoaJO/D0D3FHxq9Gz68nD0x0NPGQ+RNSLXzoAr77HTgP0nRQJBAJDi
12
+ XlMs6kRUr+Ocbb/ndD4KLhlx+7k85qTucFep92h2FfSMISLXQQPzGskbsGVzDVF6
13
+ ZBmkJswSCN5gOk/ANcUCQHJobzZpl7IQmee/qDt8ZIffVUtSuF77y4Xwso9bbcJN
14
+ Ax03Rg6rhgm0KcklEc/4VTahfNBzhF5YDkaRSTRcUsI=
15
+ -----END RSA PRIVATE KEY-----
@@ -0,0 +1,15 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ MIICXTCCAcYCCQDomedDhZaLjzANBgkqhkiG9w0BAQUFADB3MQswCQYDVQQGEwJa
3
+ QTEVMBMGA1UECAwMV2VzdGVybiBDYXBlMRIwEAYDVQQHDAlDYXBlIFRvd24xGDAW
4
+ BgNVBAoMD0hldHpuZXIgUFRZIEx0ZDEjMCEGA1UEAwwaY2VydG1laXN0ZXIuaGV0
5
+ em5lci5hZnJpY2EwHhcNMTMxMjExMTE0MTU0WhcNMTQwMTEwMTE0MTU0WjBvMQsw
6
+ CQYDVQQGEwJaQTEVMBMGA1UECAwMV2VzdGVybiBDYXBlMRIwEAYDVQQHDAlDYXBl
7
+ IFRvd24xGDAWBgNVBAoMD0hldHpuZXIgUFRZIEx0ZDEbMBkGA1UEAwwSYXhsLmhl
8
+ dHpuZXIuYWZyaWNhMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrXgWS0TD1
9
+ mKXOPkNQpMjYyLHroDRlrmVMB5GY/Uyz0eaNz5GecKa7kvrM+gdjyzr/y1vF7C27
10
+ zovu/ZJ8qQmMtIrFlAsXEETMwc6DPOIdPJnXNTlI9a8KjwBB2VtkjxaZqLt2THB1
11
+ oDbFpZHh6UeWBPvzEN1hlfBEvvnB+aQX+QIDAQABMA0GCSqGSIb3DQEBBQUAA4GB
12
+ ABl5o3iL1t9NY0MAaw53VAsM86n53WBAZEzQ+6z3vsxoKU2ejMp3GACoTsyjGWad
13
+ EyN2lKfCjPPYsmrmtshXGb/U0xHXeJS4xifibBktfwv2v2eIfrh5CbDMl6EuUzEC
14
+ mLmVOt4adTt8R8vJ42KaKULKODrsc29AHK79oyxEhYyE
15
+ -----END CERTIFICATE-----
@@ -0,0 +1,12 @@
1
+ -----BEGIN CERTIFICATE REQUEST-----
2
+ MIIBrzCCARgCAQAwbzELMAkGA1UEBhMCWkExFTATBgNVBAgMDFdlc3Rlcm4gQ2Fw
3
+ ZTESMBAGA1UEBwwJQ2FwZSBUb3duMRgwFgYDVQQKDA9IZXR6bmVyIFBUWSBMdGQx
4
+ GzAZBgNVBAMMEmF4bC5oZXR6bmVyLmFmcmljYTCBnzANBgkqhkiG9w0BAQEFAAOB
5
+ jQAwgYkCgYEAq14FktEw9Zilzj5DUKTI2Mix66A0Za5lTAeRmP1Ms9Hmjc+RnnCm
6
+ u5L6zPoHY8s6/8tbxewtu86L7v2SfKkJjLSKxZQLFxBEzMHOgzziHTyZ1zU5SPWv
7
+ Co8AQdlbZI8Wmai7dkxwdaA2xaWR4elHlgT78xDdYZXwRL75wfmkF/kCAwEAAaAA
8
+ MA0GCSqGSIb3DQEBBQUAA4GBAKHHpelQzMYFBXYa0VOWFiqRd1HXJfnUbo8D5xup
9
+ RzveAVlGTj83slgKvGigUupWdfk1S4KiUG1HsAyLcwl8lgOCO77CrdNPZC0qjB4+
10
+ pK3Xp2FMsK4+lp24FNR0tM31FA03DU8uhL8v5cvExHBn4idBEwO2W4OWPKVYKrtm
11
+ w9ne
12
+ -----END CERTIFICATE REQUEST-----
@@ -0,0 +1,15 @@
1
+ -----BEGIN RSA PRIVATE KEY-----
2
+ MIICXQIBAAKBgQCrXgWS0TD1mKXOPkNQpMjYyLHroDRlrmVMB5GY/Uyz0eaNz5Ge
3
+ cKa7kvrM+gdjyzr/y1vF7C27zovu/ZJ8qQmMtIrFlAsXEETMwc6DPOIdPJnXNTlI
4
+ 9a8KjwBB2VtkjxaZqLt2THB1oDbFpZHh6UeWBPvzEN1hlfBEvvnB+aQX+QIDAQAB
5
+ AoGBAJJSkuG821gpghHrmiDGw3RPUZRG09aqpXXY1JpUVmUIahtWTBmESOnK43mY
6
+ WCz+wB7f7jm+o4JEJ7nmLGljEHH9cvdIo84SgAeIbs1h0o28GJ1LG10L5CvTL3ai
7
+ dejFXKRNnVGxpGQQi1XqpVqv6CUSmGZoZMeTWIOPVhSddAi9AkEA17Vq6T5gJfz6
8
+ wRVpSlG1Nz2C3OQ0hPmtgwLJZcVSHUuQvgZfL8O6U7+QBQRHCHNNu0DtrdKheWHt
9
+ GdqWbWwb4wJBAMtgUsu5U/KVU3i0R8eEBkFFNho7Np4FSBQ5MMPcp727hyI/Neev
10
+ R/gFx8OSY4EBtu9Npt0fqEWVDKa/WNcU+3MCQDkm5BCwaiEmifmmhqMeSvk73vRP
11
+ smqZDJPtpRbF1R/V6Z+vaIDrRu7xjmMF4xwmEK5QYajwacATZhK3i6uqPSkCQFJj
12
+ OgdHk4dhEMiEF9PuWu5UEF+9/xkywIlYxbWCjP1im5K3EqpBqqECDnPeuKqJPEdj
13
+ KKbJbUyK3e4q891lpZsCQQDWAY3NbuNufmuWp2naACwEcYSjoMfdPoHnaaynJu5h
14
+ UHcPcCjAijtue9+iPZ/4LPJ5TH4pr8Ww4AxDCgBB8foH
15
+ -----END RSA PRIVATE KEY-----
@@ -0,0 +1,14 @@
1
+ module Certmeister
2
+ end
3
+
4
+ Dir.glob(File.join(File.dirname(__FILE__), "certmeister", "*.rb")) do |path|
5
+ require path
6
+ end
7
+
8
+ module Certmeister
9
+
10
+ def self.new(*args)
11
+ Certmeister::Base.new(*args)
12
+ end
13
+
14
+ end
@@ -0,0 +1,92 @@
1
+ require 'time'
2
+ require 'openssl'
3
+
4
+ module Certmeister
5
+
6
+ class Base
7
+
8
+ def initialize(config)
9
+ if config.valid?
10
+ @sign_policy = config.sign_policy
11
+ @fetch_policy = config.fetch_policy
12
+ @remove_policy = config.remove_policy
13
+ @ca_cert = config.ca_cert
14
+ @ca_key = config.ca_key
15
+ @store = config.store
16
+ @openssl_digest = config.openssl_digest
17
+ else
18
+ raise RuntimeError.new("invalid config")
19
+ end
20
+ end
21
+
22
+ def sign(request)
23
+ subject_to_policy(@sign_policy, request) do |request|
24
+ begin
25
+ csr = OpenSSL::X509::Request.new(request[:csr])
26
+ rescue OpenSSL::OpenSSLError => e
27
+ Certmeister::Response.error("invalid CSR (#{e.message})")
28
+ else
29
+ if get_cn(csr) == request[:cn]
30
+ pem = create_signed_certificate(csr).to_pem
31
+ @store.store(request[:cn], pem)
32
+ Certmeister::Response.hit(pem)
33
+ else
34
+ Certmeister::Response.error("CSR subject (#{get_cn(csr)}) disagrees with request CN (#{request[:cn]})")
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ def fetch(request)
41
+ subject_to_policy(@fetch_policy, request) do |request|
42
+ Certmeister::Response.new(@store.fetch(request[:cn]), nil)
43
+ end
44
+ end
45
+
46
+ def remove(request)
47
+ subject_to_policy(@remove_policy, request) do |request|
48
+ Certmeister::Response.new(!!@store.remove(request[:cn]), nil)
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ def subject_to_policy(policy, request, &block)
55
+ if !request[:cn]
56
+ Certmeister::Response.error("request missing CN")
57
+ else
58
+ authentication = policy.authenticate(request)
59
+ if authentication.authenticated?
60
+ block.call(request)
61
+ else
62
+ Certmeister::Response.error("request refused (#{authentication.error})")
63
+ end
64
+ end
65
+ end
66
+
67
+ def create_signed_certificate(csr)
68
+ cert = OpenSSL::X509::Certificate.new
69
+
70
+ cert.serial = 0
71
+ cert.version = 2
72
+ cert.subject = csr.subject
73
+ cert.public_key = csr.public_key
74
+
75
+ now = DateTime.now
76
+ cert.not_before = now.to_time
77
+ cert.not_after = (now + (5 * 365)).to_time
78
+ cert.issuer = @ca_cert.subject
79
+ cert.sign @ca_key, @openssl_digest.new
80
+
81
+ cert
82
+ end
83
+
84
+ def get_cn(csr)
85
+ if csr.subject.to_s =~ /CN=(.+)$/
86
+ $1
87
+ end
88
+ end
89
+
90
+ end
91
+
92
+ end
@@ -0,0 +1,129 @@
1
+ require 'openssl'
2
+
3
+ require 'certmeister/policy'
4
+
5
+ module Certmeister
6
+
7
+ class Config
8
+
9
+ attr_reader :store, :sign_policy, :fetch_policy, :remove_policy
10
+
11
+ def initialize(options)
12
+ @options = options
13
+ @store = options[:store]
14
+ @sign_policy = options[:sign_policy]
15
+ @fetch_policy = options[:fetch_policy]
16
+ @remove_policy = options[:remove_policy]
17
+ @errors = {}
18
+ end
19
+
20
+ def ca_cert
21
+ @ca_cert ||= OpenSSL::X509::Certificate.new(@options[:ca_cert])
22
+ end
23
+
24
+ def ca_key
25
+ @ca_key ||= OpenSSL::PKey.read(@options[:ca_key])
26
+ end
27
+
28
+ def openssl_digest
29
+ if OpenSSL::Digest.const_defined?('SHA256')
30
+ @digest = OpenSSL::Digest::SHA256
31
+ elsif OpenSSL::Digest.const_defined?('SHA1')
32
+ @digest = OpenSSL::Digest::SHA1
33
+ end
34
+ end
35
+
36
+ def valid?
37
+ validate
38
+ @errors.empty?
39
+ end
40
+
41
+ def errors
42
+ @errors
43
+ end
44
+
45
+ def error_list
46
+ @errors.keys.sort.inject([]) do |list, option|
47
+ list << "#{option} #{@errors[option]}"
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def validate
54
+ validate_x509_pem(:ca_cert)
55
+ validate_pkey_pem(:ca_key)
56
+ validate_store(:store)
57
+ validate_policy(:sign_policy)
58
+ validate_policy(:fetch_policy)
59
+ validate_policy(:remove_policy)
60
+ validate_openssl_digest
61
+ validate_known_options
62
+ end
63
+
64
+ def validate_x509_pem(option)
65
+ if not @options[option]
66
+ @errors[option] = "is required"
67
+ else
68
+ begin
69
+ OpenSSL::X509::Certificate.new(@options[option])
70
+ rescue OpenSSL::OpenSSLError => e
71
+ @errors[option] = "must be a PEM-encoded x509 certificate (#{e.message})"
72
+ end
73
+ end
74
+ end
75
+
76
+ def validate_pkey_pem(option)
77
+ if not @options[option]
78
+ @errors[option] = "is required"
79
+ else
80
+ begin
81
+ OpenSSL::PKey.read(@options[option])
82
+ rescue ArgumentError => e
83
+ @errors[option] = "must be a PEM-encoded private key (#{e.message})"
84
+ end
85
+ end
86
+ end
87
+
88
+ def validate_store(option)
89
+ o = @options[option]
90
+ if not o
91
+ @errors[option] = "is required"
92
+ elsif not o.respond_to?(:store) or o.method(:store).arity != 2
93
+ @errors[option] = "must provide a binary store method"
94
+ elsif not o.respond_to?(:fetch) or o.method(:fetch).arity != 1
95
+ @errors[option] = "must provide a unary fetch method"
96
+ elsif not o.respond_to?(:remove) or o.method(:remove).arity != 1
97
+ @errors[option] = "must provide a unary remove method"
98
+ elsif not o.respond_to?(:health_check) or o.method(:health_check).arity != 0
99
+ @errors[option] = "must provide a nullary health_check method"
100
+ end
101
+ end
102
+
103
+ def validate_policy(option)
104
+ o = @options[option]
105
+ if not o
106
+ @errors[option] = "is required"
107
+ elsif not Certmeister::Policy.validate_authenticate_signature(@options[option])
108
+ @errors[option] = "must provide a unary authenticate method"
109
+ elsif not Certmeister::Policy.validate_authenticate_returns_response(@options[option])
110
+ @errors[option] = "must return a policy response"
111
+ end
112
+ end
113
+
114
+ def validate_openssl_digest
115
+ if not openssl_digest
116
+ @errors[:openssl_digest] = "can't find FIPS 140-2 compliant algorithm in OpenSSL::Digest"
117
+ end
118
+ end
119
+
120
+ def validate_known_options
121
+ unexpected = @options.keys - [:ca_cert, :ca_key, :store, :sign_policy, :fetch_policy, :remove_policy]
122
+ unexpected.each do |option|
123
+ @errors[option] = "is not a supported option"
124
+ end
125
+ end
126
+
127
+ end
128
+
129
+ end