r509-ocsp-responder 0.3.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 (55) hide show
  1. data/README.md +77 -0
  2. data/Rakefile +38 -0
  3. data/doc/R509.html +115 -0
  4. data/doc/R509/Ocsp.html +130 -0
  5. data/doc/R509/Ocsp/Helper.html +126 -0
  6. data/doc/R509/Ocsp/Helper/RequestChecker.html +739 -0
  7. data/doc/R509/Ocsp/Helper/ResponseSigner.html +583 -0
  8. data/doc/R509/Ocsp/Responder.html +129 -0
  9. data/doc/R509/Ocsp/Responder/OcspConfig.html +289 -0
  10. data/doc/R509/Ocsp/Responder/Server.html +128 -0
  11. data/doc/R509/Ocsp/Responder/StatusError.html +134 -0
  12. data/doc/R509/Ocsp/Signer.html +584 -0
  13. data/doc/_index.html +197 -0
  14. data/doc/class_list.html +53 -0
  15. data/doc/css/common.css +1 -0
  16. data/doc/css/full_list.css +57 -0
  17. data/doc/css/style.css +328 -0
  18. data/doc/file.README.html +156 -0
  19. data/doc/file_list.html +55 -0
  20. data/doc/frames.html +28 -0
  21. data/doc/index.html +156 -0
  22. data/doc/js/app.js +214 -0
  23. data/doc/js/full_list.js +173 -0
  24. data/doc/js/jquery.js +4 -0
  25. data/doc/method_list.html +164 -0
  26. data/doc/top-level-namespace.html +112 -0
  27. data/lib/r509/ocsp/responder/ocsp-config.rb +35 -0
  28. data/lib/r509/ocsp/responder/server.rb +169 -0
  29. data/lib/r509/ocsp/responder/version.rb +7 -0
  30. data/lib/r509/ocsp/signer.rb +244 -0
  31. data/spec/fixtures.rb +196 -0
  32. data/spec/fixtures/cert1.pem +24 -0
  33. data/spec/fixtures/config_test_various.yaml +46 -0
  34. data/spec/fixtures/ocsptest.r509.local.pem +27 -0
  35. data/spec/fixtures/second_ca.cer +26 -0
  36. data/spec/fixtures/second_ca.key +27 -0
  37. data/spec/fixtures/stca.pem +22 -0
  38. data/spec/fixtures/stca_ocsp_request.der +0 -0
  39. data/spec/fixtures/stca_ocsp_response.der +0 -0
  40. data/spec/fixtures/test_ca.cer +22 -0
  41. data/spec/fixtures/test_ca.key +28 -0
  42. data/spec/fixtures/test_ca_ocsp.cer +26 -0
  43. data/spec/fixtures/test_ca_ocsp.key +27 -0
  44. data/spec/fixtures/test_ca_ocsp_chain.txt +48 -0
  45. data/spec/fixtures/test_ca_request.der +0 -0
  46. data/spec/fixtures/test_ca_response.der +0 -0
  47. data/spec/fixtures/test_ca_subroot.cer +25 -0
  48. data/spec/fixtures/test_ca_subroot.key +27 -0
  49. data/spec/fixtures/test_ca_subroot_ocsp.cer +25 -0
  50. data/spec/fixtures/test_ca_subroot_ocsp.key +27 -0
  51. data/spec/fixtures/test_config.yaml +17 -0
  52. data/spec/server_spec.rb +400 -0
  53. data/spec/signer_spec.rb +275 -0
  54. data/spec/spec_helper.rb +18 -0
  55. metadata +259 -0
@@ -0,0 +1,35 @@
1
+ module R509::Ocsp::Responder
2
+ class OcspConfig
3
+ def self.load_config
4
+ config_data = File.read("config.yaml")
5
+
6
+ Dependo::Registry[:config_pool] = R509::Config::CaConfigPool.from_yaml("certificate_authorities", config_data)
7
+
8
+ Dependo::Registry[:copy_nonce] = YAML.load(config_data)["copy_nonce"] || false
9
+
10
+ Dependo::Registry[:cache_headers] = YAML.load(config_data)["cache_headers"] || false
11
+
12
+ Dependo::Registry[:max_cache_age] = YAML.load(config_data)["max_cache_age"]
13
+
14
+ Dependo::Registry[:ocsp_signer] = R509::Ocsp::Signer.new(
15
+ :configs => Dependo::Registry[:config_pool],
16
+ :validity_checker => R509::Validity::Redis::Checker.new(Dependo::Registry[:redis]),
17
+ :copy_nonce => Dependo::Registry[:copy_nonce]
18
+ )
19
+ end
20
+
21
+ def self.print_config
22
+ Dependo::Registry[:log].warn "Config loaded"
23
+ Dependo::Registry[:log].warn "Copy Nonce: "+Dependo::Registry[:copy_nonce].to_s
24
+ Dependo::Registry[:log].warn "Cache Headers: "+Dependo::Registry[:cache_headers].to_s
25
+ Dependo::Registry[:log].warn "Max Cache Age: "+Dependo::Registry[:max_cache_age].to_s
26
+ Dependo::Registry[:config_pool].all.each do |config|
27
+ Dependo::Registry[:log].warn "Config: "
28
+ Dependo::Registry[:log].warn "CA Cert:"+config.ca_cert.subject.to_s
29
+ Dependo::Registry[:log].warn "OCSP Cert (may be the same as above):"+config.ocsp_cert.subject.to_s
30
+ Dependo::Registry[:log].warn "OCSP Validity Hours: "+config.ocsp_validity_hours.to_s
31
+ Dependo::Registry[:log].warn "\n"
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,169 @@
1
+ require 'rubygems' if RUBY_VERSION < "1.9"
2
+ require 'sinatra/base'
3
+ require 'r509'
4
+ require 'r509/ocsp/signer'
5
+ require 'r509/validity/redis'
6
+ require 'base64'
7
+ require 'dependo'
8
+ require 'logger'
9
+ require 'time'
10
+ require File.dirname(__FILE__)+'/ocsp-config.rb'
11
+
12
+ # Capture USR2 calls so we can reload and print the config
13
+ # I'd rather use HUP, but daemons like thin already capture that
14
+ # so we can't use it.
15
+ Signal.trap("USR2") do
16
+ R509::Ocsp::Responder::OcspConfig.load_config
17
+ R509::Ocsp::Responder::OcspConfig.print_config
18
+ end
19
+
20
+
21
+ module R509::Ocsp::Responder
22
+ #error for status checking
23
+ class StatusError < StandardError
24
+ end
25
+
26
+ class Server < Sinatra::Base
27
+ include Dependo::Mixin
28
+
29
+ configure do
30
+ mime_type :ocsp, 'application/ocsp-response'
31
+ disable :protection #disable Rack::Protection (for speed)
32
+ disable :logging
33
+ set :environment, :production
34
+ end
35
+
36
+ error do
37
+ log.error env["sinatra.error"].inspect
38
+ log.error env["sinatra.error"].backtrace.join("\n")
39
+ "Something is amiss with our OCSP responder. You should ... wait?"
40
+ end
41
+
42
+ error OpenSSL::OCSP::OCSPError do
43
+ "Invalid request"
44
+ end
45
+
46
+ error R509::Ocsp::Responder::StatusError do
47
+ "Down"
48
+ end
49
+
50
+ get '/favicon.ico' do
51
+ log.debug "go away. no children."
52
+ "go away. no children"
53
+ end
54
+
55
+ get '/status/?' do
56
+ begin
57
+ if Dependo::Registry[:ocsp_signer].validity_checker.is_available?
58
+ "OK"
59
+ else
60
+ raise R509::Ocsp::Responder::StatusError
61
+ end
62
+ rescue
63
+ raise R509::Ocsp::Responder::StatusError
64
+ end
65
+ end
66
+
67
+ get '/*' do
68
+ raw_request = params[:splat].join("/")
69
+ #remove any leading slashes (looking at you MS Crypto API)
70
+ raw_request.sub!(/^\/+/,"")
71
+ log.info { "GET Request: "+raw_request }
72
+ der = Base64.decode64(raw_request)
73
+ request_response = handle_ocsp_request(der, "GET")
74
+ build_headers(request_response)
75
+ request_response[:response].to_der
76
+ end
77
+
78
+ post '/' do
79
+ if request.media_type == 'application/ocsp-request'
80
+ der = request.env["rack.input"].read
81
+ log.info { "POST Request: "+Base64.encode64(der).gsub!(/\n/,"") }
82
+ request_response = handle_ocsp_request(der, "POST")
83
+ request_response[:response].to_der
84
+ end
85
+ end
86
+
87
+ private
88
+
89
+ def handle_ocsp_request(der, method)
90
+ begin
91
+ request_response = ocsp_signer.handle_request(der)
92
+
93
+ log_ocsp_response(request_response[:response],method)
94
+
95
+ content_type :ocsp
96
+ request_response
97
+ rescue StandardError => e
98
+ log.error "unexpected error #{e}"
99
+ raise e
100
+ end
101
+ end
102
+
103
+ def log_ocsp_response(ocsp_response, method="?")
104
+ if response.nil?
105
+ log.error "Something went horribly wrong"
106
+ return
107
+ end
108
+
109
+ case ocsp_response.status
110
+ when OpenSSL::OCSP::RESPONSE_STATUS_SUCCESSFUL
111
+ serial_data = ocsp_response.basic.status.map do |status|
112
+ friendly_status = case status[1]
113
+ when 0
114
+ "VALID"
115
+ when 1
116
+ "REVOKED"
117
+ when 2
118
+ "UNKNOWN"
119
+ end
120
+ if ocsp_response.basic.status[0][0].respond_to?(:issuer_key_hash)
121
+ config_used = ocsp_signer.request_checker.configs_hash[ocsp_response.basic.status[0][0].issuer_key_hash]
122
+ else
123
+ config_used = ocsp_signer.request_checker.configs.find do |config|
124
+ #we need to create an OCSP::CertificateId object that has the right
125
+ #issuer so we can pass it to #cmp_issuer. This is annoying because
126
+ #CertificateId wants a cert and its issuer, but we don't want to
127
+ #force users to provide an end entity cert just to make this comparison
128
+ #work. So, we create a fake new cert and pass it in.
129
+ ee_cert = OpenSSL::X509::Certificate.new
130
+ ee_cert.issuer = config.ca_cert.cert.subject
131
+ issuer_certid = OpenSSL::OCSP::CertificateId.new(ee_cert,config.ca_cert.cert)
132
+ ocsp_response.basic.status[0][0].cmp_issuer(issuer_certid)
133
+ end
134
+ end
135
+ stats.record(config_used.ca_cert.subject.to_s, status[0].serial.to_s, friendly_status) if Dependo::Registry.has_key?(:stats)
136
+ status[0].serial.to_s+" Status: #{friendly_status}"
137
+ end
138
+ log.info { "#{method} Request For Serial(s): #{serial_data.join(",")} UserAgent: #{env["HTTP_USER_AGENT"]}" }
139
+ when OpenSSL::OCSP::RESPONSE_STATUS_UNAUTHORIZED
140
+ log.info { "#{method} Request For Unauthorized CA. UserAgent: #{env["HTTP_USER_AGENT"]}" }
141
+ when OpenSSL::OCSP::RESPONSE_STATUS_MALFORMEDREQUEST
142
+ log.info { "#{method} Malformed Request. UserAgent: #{env["HTTP_USER_AGENT"]}" }
143
+ end
144
+ end
145
+
146
+ def build_headers(request_response)
147
+ ocsp_response = request_response[:response]
148
+ ocsp_request = request_response[:request]
149
+
150
+ # cache_headers is injected via config.ru
151
+ # we only cache if it's a RESPONSE_STATUS_SUCCESSFUL response and there's no nonce.
152
+ if cache_headers and not ocsp_response.basic.nil? and ocsp_response.check_nonce(ocsp_request) == R509::Ocsp::Request::Nonce::BOTH_ABSENT
153
+ calculated_max_age = ocsp_response.basic.status[0][5] - Time.now
154
+ #same with max_cache_age
155
+ if not max_cache_age or ( max_cache_age > calculated_max_age )
156
+ max_age = calculated_max_age
157
+ else
158
+ max_age = max_cache_age
159
+ end
160
+
161
+ response["Last-Modified"] = Time.now.httpdate
162
+ response["ETag"] = OpenSSL::Digest::SHA1.new(ocsp_response.to_der).to_s
163
+ response["Expires"] = ocsp_response.basic.status[0][5].httpdate
164
+ response["Cache-Control"] = "max-age=#{max_age.to_i}, public, no-transform, must-revalidate"
165
+ end
166
+ end
167
+
168
+ end
169
+ end
@@ -0,0 +1,7 @@
1
+ module R509
2
+ module Ocsp
3
+ module Responder
4
+ VERSION="0.3.1"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,244 @@
1
+ require 'openssl'
2
+ require 'r509/exceptions'
3
+ require 'r509/config'
4
+ require 'dependo'
5
+
6
+ # OCSP related classes (signing, response, request)
7
+ module R509::Ocsp
8
+ # A class for signing OCSP responses
9
+ class Signer
10
+ attr_reader :validity_checker,:request_checker
11
+
12
+ # @option options [Boolean] :copy_nonce copy nonce from request to response?
13
+ # @option options [R509::Config::CaConfigPool] :configs CaConfigPool object
14
+ # possible OCSP issuance roots that we want to issue OCSP responses for
15
+ def initialize(options)
16
+ if options.has_key?(:validity_checker)
17
+ @validity_checker = options[:validity_checker]
18
+ else
19
+ @validity_checker = R509::Validity::DefaultChecker.new
20
+ end
21
+ @request_checker = Helper::RequestChecker.new(options[:configs], @validity_checker)
22
+ @response_signer = Helper::ResponseSigner.new(options)
23
+ end
24
+
25
+
26
+ # @param request [String,OpenSSL::OCSP::Request] OCSP request (string or parsed object)
27
+ # @return [Hash]
28
+ # * :request [OpenSSL::OCSP::Request] parsed request object
29
+ # * :response [OpenSSL::OCSP::Response] full response object
30
+ def handle_request(request)
31
+ begin
32
+ parsed_request = OpenSSL::OCSP::Request.new request
33
+ rescue
34
+ return {:response => @response_signer.create_response(OpenSSL::OCSP::RESPONSE_STATUS_MALFORMEDREQUEST), :request => nil}
35
+ end
36
+
37
+ statuses = @request_checker.check_statuses(parsed_request)
38
+ if not @request_checker.validate_statuses(statuses)
39
+ return {:response => @response_signer.create_response(OpenSSL::OCSP::RESPONSE_STATUS_UNAUTHORIZED), :request => nil}
40
+ end
41
+
42
+ basic_response = @response_signer.create_basic_response(parsed_request,statuses)
43
+
44
+ {:response => @response_signer.create_response(
45
+ OpenSSL::OCSP::RESPONSE_STATUS_SUCCESSFUL,
46
+ basic_response
47
+ ), :request => parsed_request}
48
+ end
49
+
50
+ end
51
+ end
52
+
53
+ #Helper module for OCSP handling
54
+ module R509::Ocsp::Helper
55
+ # checks requests for validity against a set of configs
56
+ class RequestChecker
57
+ include Dependo::Mixin
58
+ attr_reader :configs,:configs_hash
59
+
60
+ # @param [R509::Config::CaConfigPool] configs CaConfigPool object
61
+ # @param [R509::Validity::Checker] validity_checker an implementation of the R509::Validity::Checker class
62
+ def initialize(configs, validity_checker)
63
+ unless configs.kind_of?(R509::Config::CaConfigPool)
64
+ raise R509::R509Error, "Must pass R509::Config::CaConfigPool object"
65
+ end
66
+ if configs.all.empty?
67
+ raise R509::R509Error, "Must be at least one R509::Config object"
68
+ end
69
+ @configs = configs.all
70
+ test_cid = OpenSSL::OCSP::CertificateId.new(OpenSSL::X509::Certificate.new,OpenSSL::X509::Certificate.new)
71
+ if test_cid.respond_to?(:issuer_key_hash)
72
+ @configs_hash = {}
73
+ @configs.each do |config|
74
+ ee_cert = OpenSSL::X509::Certificate.new
75
+ ee_cert.issuer = config.ca_cert.cert.subject
76
+ # per RFC 5019
77
+ # Clients MUST use SHA1 as the hashing algorithm for the
78
+ # CertID.issuerNameHash and the CertID.issuerKeyHash values.
79
+ # so we can safely assume that our inbound hashes will be SHA1
80
+ issuer_certid = OpenSSL::OCSP::CertificateId.new(ee_cert,config.ca_cert.cert,OpenSSL::Digest::SHA1.new)
81
+ @configs_hash[issuer_certid.issuer_key_hash] = config
82
+ end
83
+ end
84
+ @validity_checker = validity_checker
85
+ if @validity_checker.nil?
86
+ raise R509::R509Error, "Must supply a R509::Validity::Checker"
87
+ end
88
+ if not @validity_checker.respond_to?(:check)
89
+ raise R509::R509Error, "The validity checker must have a check method"
90
+ end
91
+ end
92
+
93
+ # Loads and checks a raw OCSP request
94
+ #
95
+ # @param request [OpenSSL::OCSP::Request] OpenSSL OCSP Request object
96
+ # @return [Hash] hash from the check_status method
97
+ def check_statuses(request)
98
+ request.certid.map { |certid|
99
+ if certid.respond_to?(:issuer_key_hash)
100
+ validated_config = @configs_hash[certid.issuer_key_hash]
101
+ else
102
+ validated_config = @configs.find do |config|
103
+ #we need to create an OCSP::CertificateId object that has the right
104
+ #issuer so we can pass it to #cmp_issuer. This is annoying because
105
+ #CertificateId wants a cert and its issuer, but we don't want to
106
+ #force users to provide an end entity cert just to make this comparison
107
+ #work. So, we create a fake new cert and pass it in.
108
+ ee_cert = OpenSSL::X509::Certificate.new
109
+ ee_cert.issuer = config.ca_cert.cert.subject
110
+ issuer_certid = OpenSSL::OCSP::CertificateId.new(ee_cert,config.ca_cert.cert)
111
+ certid.cmp_issuer(issuer_certid)
112
+ end
113
+ end
114
+
115
+ log.info "#{validated_config.ca_cert.subject.to_s} found for issuer" if validated_config
116
+ check_status(certid, validated_config)
117
+ }
118
+ end
119
+
120
+ # Determines whether the statuses constitute a request that is compliant.
121
+ # No config means we don't know the CA, different configs means there are
122
+ # requests from two different CAs in there. Both are invalid.
123
+ #
124
+ # @param statuses [Array<Hash>] array of hashes from check_statuses
125
+ # @return [Boolean]
126
+ def validate_statuses(statuses)
127
+ validity = true
128
+ config = nil
129
+
130
+ statuses.each do |status|
131
+ if status[:config].nil?
132
+ validity = false
133
+ end
134
+ if config.nil?
135
+ config = status[:config]
136
+ end
137
+ if config != status[:config]
138
+ validity = false
139
+ end
140
+ end
141
+
142
+ validity
143
+ end
144
+
145
+ private
146
+
147
+ # Checks the status of a certificate with the corresponding CA
148
+ # @param certid [OpenSSL::OCSP::CertificateId] The certId object from check_statuses
149
+ # @param validated_config [R509::Config]
150
+ def check_status(certid, validated_config)
151
+ if(validated_config == nil) then
152
+ return {
153
+ :certid => certid,
154
+ :config => nil
155
+ }
156
+ else
157
+ validity_status = @validity_checker.check(validated_config.ca_cert.subject.to_s,certid.serial)
158
+ return {
159
+ :certid => certid,
160
+ :status => validity_status.ocsp_status,
161
+ :revocation_reason => validity_status.revocation_reason,
162
+ :revocation_time => validity_status.revocation_time,
163
+ :config => validated_config
164
+ }
165
+ end
166
+ end
167
+ end
168
+
169
+ #signs OCSP responses
170
+ class ResponseSigner
171
+ # @option options [Boolean] :copy_nonce
172
+ def initialize(options)
173
+ if options.has_key?(:copy_nonce)
174
+ @copy_nonce = options[:copy_nonce]
175
+ else
176
+ @copy_nonce = false
177
+ end
178
+ end
179
+
180
+ # It is UNWISE to call this method directly because it assumes that the request is
181
+ # validated. You probably want to take a look at R509::Ocsp::Signer#handle_request
182
+ #
183
+ # @param request [OpenSSL::OCSP::Request]
184
+ # @param statuses [Hash] hash from R509::Ocsp::Helper::RequestChecker#check_statuses
185
+ # @return [OpenSSL::OCSP::BasicResponse]
186
+ def create_basic_response(request,statuses)
187
+ basic_response = OpenSSL::OCSP::BasicResponse.new
188
+
189
+ basic_response.copy_nonce(request) if @copy_nonce
190
+
191
+ statuses.each do |status|
192
+ #revocation time is retarded and is relative to now, so
193
+ #let's figure out what that is.
194
+ if status[:status] == OpenSSL::OCSP::V_CERTSTATUS_REVOKED
195
+ revocation_time = status[:revocation_time].to_i - Time.now.to_i
196
+ end
197
+ basic_response.add_status(status[:certid],
198
+ status[:status],
199
+ status[:revocation_reason],
200
+ revocation_time,
201
+ -1*status[:config].ocsp_start_skew_seconds,
202
+ status[:config].ocsp_validity_hours*3600,
203
+ [] #array of OpenSSL::X509::Extensions
204
+ )
205
+ end
206
+
207
+ #this method assumes the request data is validated by validate_request so all configs will be the same and
208
+ #we can choose to use the first one safely
209
+ config = statuses[0][:config]
210
+
211
+ #confusing, but R509::Cert contains R509::PrivateKey under #key. PrivateKey#key gives the OpenSSL object
212
+ #turns out BasicResponse#sign can take up to 4 params
213
+ #cert, key, array of OpenSSL::X509::Certificates, flags (not sure what the enumeration of those are)
214
+ basic_response.sign(config.ocsp_cert.cert,config.ocsp_cert.key.key,config.ocsp_chain)
215
+ end
216
+
217
+ # Builds final response.
218
+ #
219
+ # @param response_status [OpenSSL::OCSP::RESPONSE_STATUS_*] the primary response status
220
+ # @param basic_response [OpenSSL::OCSP::BasicResponse] an optional basic response object
221
+ # generated by create_basic_response
222
+ # @return [OpenSSL::OCSP::OCSPResponse]
223
+ def create_response(response_status,basic_response=nil)
224
+
225
+ # first arg is the response status code, comes from this list
226
+ # these can also be enumerated via OpenSSL::OCSP::RESPONSE_STATUS_*
227
+ #OCSPResponseStatus ::= ENUMERATED {
228
+ # successful (0), --Response has valid confirmations
229
+ # malformedRequest (1), --Illegal confirmation request
230
+ # internalError (2), --Internal error in issuer
231
+ # tryLater (3), --Try again later
232
+ # --(4) is not used
233
+ # sigRequired (5), --Must sign the request
234
+ # unauthorized (6) --Request unauthorized
235
+ #}
236
+ #
237
+ R509::Ocsp::Response.new(
238
+ OpenSSL::OCSP::Response.create(
239
+ response_status, basic_response
240
+ )
241
+ )
242
+ end
243
+ end
244
+ end