aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorKazuki Yamaguchi <k@rhe.jp>2016-06-11 14:59:39 +0900
committerKazuki Yamaguchi <k@rhe.jp>2017-02-17 01:47:32 +0900
commit4172c17790accddb3d84f8a234c99ea78a9658ca (patch)
treec62b639f834de2b303278fe8c953b655d5917c73
parent732318548f7c0e58f48c1baed3ed63b49a23e121 (diff)
downloadruby-openssl-ky/ocsp-stapling.tar.gz
ssl: add basic support for OCSP staplingky/ocsp-stapling
Add SSL::SSLContext#ocsp_request_cb and #ocsp_response_cb. oscp_request_cb is invoked in the server, when the client requested a certificate status by status_request extension. In client mode, setting ocsp_response_cb adds status_request extension in the Client Hello and it is invoked when the server respond with a OCSP response.
-rw-r--r--ext/openssl/ossl_ocsp.c24
-rw-r--r--ext/openssl/ossl_ocsp.h3
-rw-r--r--ext/openssl/ossl_ssl.c165
-rw-r--r--test/test_ssl.rb81
4 files changed, 272 insertions, 1 deletions
diff --git a/ext/openssl/ossl_ocsp.c b/ext/openssl/ossl_ocsp.c
index a8b3503d..c860a989 100644
--- a/ext/openssl/ossl_ocsp.c
+++ b/ext/openssl/ossl_ocsp.c
@@ -475,6 +475,30 @@ ossl_ocspreq_to_der(VALUE self)
/*
* OCSP::Response
*/
+OCSP_RESPONSE *
+GetOCSPResPtr(VALUE obj)
+{
+ OCSP_RESPONSE *res;
+
+ SafeGetOCSPRes(obj, res);
+
+ return res;
+}
+
+VALUE
+ossl_ocspres_new(OCSP_RESPONSE *res)
+{
+ OCSP_RESPONSE *res_new;
+ VALUE obj;
+
+ obj = NewOCSPRes(cOCSPRes);
+ res_new = ASN1_item_dup(ASN1_ITEM_rptr(OCSP_RESPONSE), res);
+ if (!res_new)
+ ossl_raise(eOCSPError, "ASN1_item_dup");
+ SetOCSPRes(obj, res_new);
+
+ return obj;
+}
/* call-seq:
* OpenSSL::OCSP::Response.create(status, basic_response = nil) -> response
diff --git a/ext/openssl/ossl_ocsp.h b/ext/openssl/ossl_ocsp.h
index 21e2c99a..4512c97b 100644
--- a/ext/openssl/ossl_ocsp.h
+++ b/ext/openssl/ossl_ocsp.h
@@ -16,6 +16,9 @@ extern VALUE mOCSP;
extern VALUE cOPCSReq;
extern VALUE cOPCSRes;
extern VALUE cOPCSBasicRes;
+
+OCSP_RESPONSE *GetOCSPResPtr(VALUE);
+VALUE ossl_ocspres_new(OCSP_RESPONSE *);
#endif
void Init_ossl_ocsp(void);
diff --git a/ext/openssl/ossl_ssl.c b/ext/openssl/ossl_ssl.c
index c0063d69..b6da4dbb 100644
--- a/ext/openssl/ossl_ssl.c
+++ b/ext/openssl/ossl_ssl.c
@@ -43,7 +43,7 @@ static ID id_i_cert_store, id_i_ca_file, id_i_ca_path, id_i_verify_mode,
id_i_session_id_context, id_i_session_get_cb, id_i_session_new_cb,
id_i_session_remove_cb, id_i_npn_select_cb, id_i_npn_protocols,
id_i_alpn_select_cb, id_i_alpn_protocols, id_i_servername_cb,
- id_i_verify_hostname;
+ id_i_verify_hostname, id_i_ocsp_request_cb, id_i_ocsp_response_cb;
static ID id_i_io, id_i_context, id_i_hostname;
/*
@@ -725,6 +725,104 @@ ssl_info_cb(const SSL *ssl, int where, int val)
}
}
+#if !defined(OPENSSL_NO_OCSP) && defined(SSL_CTX_set_tlsext_status_cb)
+static VALUE
+call_ocsp_req_cb(VALUE ssl_obj)
+{
+ SSL *ssl;
+ OCSP_RESPONSE *resp;
+ VALUE ctx_obj, cb_proc, ret;
+ unsigned char *resp_der = NULL;
+ int len;
+
+ GetSSL(ssl_obj, ssl);
+ ctx_obj = rb_attr_get(ssl_obj, id_i_context);
+ cb_proc = rb_attr_get(ctx_obj, id_i_ocsp_request_cb);
+
+ ret = rb_funcall(cb_proc, rb_intern("call"), 1, ssl_obj);
+ if (NIL_P(ret))
+ return Qfalse;
+
+ resp = GetOCSPResPtr(ret);
+ if ((len = i2d_OCSP_RESPONSE(resp, &resp_der)) <= 0)
+ ossl_raise(eSSLError, "i2d_OCSP_RESPONSE");
+ SSL_set_tlsext_status_ocsp_resp(ssl, resp_der, len);
+
+ return Qtrue;
+}
+
+static VALUE
+call_ocsp_res_cb(VALUE ssl_obj)
+{
+ SSL *ssl;
+ OCSP_RESPONSE *resp;
+ VALUE ctx_obj, cb_proc, res_obj;
+ const unsigned char *p;
+ int len, status;
+
+ GetSSL(ssl_obj, ssl);
+ ctx_obj = rb_attr_get(ssl_obj, id_i_context);
+ cb_proc = rb_attr_get(ctx_obj, id_i_ocsp_response_cb);
+
+ if ((len = SSL_get_tlsext_status_ocsp_resp(ssl, &p)) == -1)
+ res_obj = Qnil;
+ else {
+ if (!(resp = d2i_OCSP_RESPONSE(NULL, &p, len)))
+ ossl_raise(eSSLError, "d2i_OCSP_RESPONSE");
+ res_obj = rb_protect((VALUE (*)(VALUE))ossl_ocspres_new, (VALUE)resp, &status);
+ OCSP_RESPONSE_free(resp);
+ if (status)
+ rb_jump_tag(status);
+ }
+
+ return rb_funcall(cb_proc, rb_intern("call"), 2, ssl_obj, res_obj);
+}
+
+static int
+ssl_status_cb(SSL *ssl, void *unused_arg)
+{
+ /*
+ * For a server:
+ * set an OCSP response with SSL_set_tlsext_status_ocsp_resp() and return
+ * SSL_TLSEXT_ERR_OK (when we set), SSL_TLSEXT_ERR_NOACK (when we don't)
+ * or SSL_TLSEXT_ERR_ALERT_FATAL (error).
+ *
+ * For a client:
+ * get the OCSP response with SSL_get_tlsext_status_ocsp_resp() and return a
+ * positive value (when acceptable), zero (not acceptable) or a negative
+ * value (error).
+ */
+ VALUE ret, ssl_obj;
+ int status = 0, is_server;
+
+ is_server = SSL_is_server(ssl);
+ ssl_obj = (VALUE)SSL_get_ex_data(ssl, ossl_ssl_ex_ptr_idx);
+ if (!ssl_obj)
+ goto err;
+
+ if (is_server) {
+ ret = rb_protect(call_ocsp_req_cb, ssl_obj, &status);
+ if (status)
+ goto err;
+
+ return RTEST(ret) ? SSL_TLSEXT_ERR_OK : SSL_TLSEXT_ERR_NOACK;
+ }
+ else {
+ ret = rb_protect(call_ocsp_res_cb, ssl_obj, &status);
+ if (status)
+ goto err;
+
+ return RTEST(ret) ? 1 : 0;
+ }
+
+err:
+ if (status)
+ rb_ivar_set(ssl_obj, ID_callback_state, INT2NUM(status));
+
+ return is_server ? SSL_TLSEXT_ERR_ALERT_FATAL : -1;
+}
+#endif
+
/*
* Gets various OpenSSL options.
*/
@@ -948,6 +1046,15 @@ ossl_sslctx_setup(VALUE self)
OSSL_Debug("SSL TLSEXT servername callback added");
}
+#if !defined(OPENSSL_NO_OCSP) && defined(SSL_CTX_set_tlsext_status_cb)
+ if (!NIL_P(rb_attr_get(self, id_i_ocsp_request_cb)) ||
+ !NIL_P(rb_attr_get(self, id_i_ocsp_response_cb))) {
+ /* SSL_CTX_set_tlsext_status_type() does not exist in OpenSSL < 1.1.0...
+ * Setting in ossl_ssl_initialize(). */
+ SSL_CTX_set_tlsext_status_cb(ctx, ssl_status_cb);
+ }
+#endif
+
return Qtrue;
}
@@ -1459,6 +1566,12 @@ ossl_ssl_initialize(int argc, VALUE *argv, VALUE self)
verify_cb = rb_attr_get(v_ctx, id_i_verify_callback);
SSL_set_ex_data(ssl, ossl_ssl_ex_vcb_idx, (void *)verify_cb);
+#if !defined(OPENSSL_NO_OCSP) && defined(SSL_CTX_set_tlsext_status_cb)
+ /* This can't be set in SSL_CTX */
+ if (!NIL_P(rb_attr_get(v_ctx, id_i_ocsp_response_cb)))
+ SSL_set_tlsext_status_type(ssl, TLSEXT_STATUSTYPE_ocsp);
+#endif
+
rb_call_super(0, NULL);
return self;
@@ -2545,6 +2658,54 @@ Init_ossl_ssl(void)
rb_attr(cSSLContext, rb_intern("alpn_select_cb"), 1, 1, Qfalse);
#endif
+#if !defined(OPENSSL_NO_OCSP) && defined(SSL_CTX_set_tlsext_status_cb)
+ /*
+ * A callback invoked on the server side when the client requested OCSP
+ * certificate status. The callback may return an OpenSSL::OCSP::Response or
+ * nil, if there is no suitable OCSP response. Exceptions raised in the
+ * callback cause the handshake to fail.
+ *
+ * === Example
+ *
+ * ctx.ocsp_request_cb = proc do |ssl|
+ * cert = ssl.cert
+ * # find a response for cert
+ * ocsp_response
+ * end
+ */
+ rb_attr(cSSLContext, rb_intern("ocsp_request_cb"), 1, 1, Qfalse);
+ /*
+ * A callback invoked on the client side when the server returns the OCSP
+ * certificate status. The callback receives two values +ssl+, the
+ * OpenSSL::SSL:SSLSocket, and +ocsp_response+, the OCSP response. The OSCP
+ * response may be nil when the server doesn't provide. The response must be
+ * verified in this callback. The callback must return a boolean value. A
+ * true return means successful verification, a false return means there are
+ * errors in the OCSP response. A false return or an exception aborts the
+ * handshake.
+ *
+ * === Example
+ *
+ * ctx.ocsp_response_cb = proc do |ssl, response|
+ * next true unless response
+ * next false unless response.status == OpenSSL::OCSP::RESPONSE_STATUS_SUCCESSFUL
+ * basic = response.basic
+ *
+ * # verify the signature on BasicResponse
+ * next false unless basic.verify([], ssl.cert_store)
+ *
+ * certid = OpenSSL::OCSP::CertificateId.new(*ssl.peer_cert_chain.take(2))
+ * single = basic.find_response(certid)
+ * next false unless single
+ * next false unless single.cert_status == OpenSSL::OCSP::V_CERTSTATUS_GOOD
+ *
+ * # validate the time when the response was issued
+ * single.check_validity
+ * end
+ */
+ rb_attr(cSSLContext, rb_intern("ocsp_response_cb"), 1, 1, Qfalse);
+#endif
+
rb_define_alias(cSSLContext, "ssl_timeout", "timeout");
rb_define_alias(cSSLContext, "ssl_timeout=", "timeout=");
rb_define_method(cSSLContext, "ssl_version=", ossl_sslctx_set_ssl_version, 1);
@@ -2743,6 +2904,8 @@ Init_ossl_ssl(void)
DefIVarID(alpn_select_cb);
DefIVarID(servername_cb);
DefIVarID(verify_hostname);
+ DefIVarID(ocsp_request_cb);
+ DefIVarID(ocsp_response_cb);
DefIVarID(io);
DefIVarID(context);
diff --git a/test/test_ssl.rb b/test/test_ssl.rb
index 8bf0c214..a2643155 100644
--- a/test/test_ssl.rb
+++ b/test/test_ssl.rb
@@ -1277,6 +1277,78 @@ end
}
end
+ def test_ocsp_stapling
+ issue_ocsp_signer
+ req_called, res_called = nil
+
+ bres = OpenSSL::OCSP::BasicResponse.new
+ cid = OpenSSL::OCSP::CertificateId.new(@svr_cert, @ca_cert, "sha1")
+ bres.add_status(cid, OpenSSL::OCSP::V_CERTSTATUS_GOOD, 0, nil, -300, 500, [])
+ bres.sign(@ocsp_cert, @ocsp_key, [], 0)
+ res = OpenSSL::OCSP::Response.create(OpenSSL::OCSP::RESPONSE_STATUS_SUCCESSFUL, bres)
+
+ ctx_proc = proc { |ctx|
+ ctx.ocsp_request_cb = proc { |ssl|
+ req_called = true
+ res
+ }
+ }
+ start_server(ctx_proc: ctx_proc) do |svr, port|
+ ctx = OpenSSL::SSL::SSLContext.new
+ ctx.verify_mode = OpenSSL::SSL::VERIFY_PEER
+ store = OpenSSL::X509::Store.new
+ store.add_cert(@ca_cert)
+ ctx.cert_store = store
+ ctx.ocsp_response_cb = proc { |ssl, received|
+ res_called = true
+ assert_not_nil received
+ assert_equal res.to_der, received.to_der
+ true
+ }
+ server_connect(port, ctx) { }
+ end
+
+ assert_equal true, req_called
+ assert_equal true, res_called
+ end
+
+ def test_ocsp_stapling_noack
+ called = false
+
+ ctx_proc = proc { |ctx| ctx.ocsp_request_cb = proc { |ssl| nil } }
+ start_server(ctx_proc: ctx_proc) do |svr, port|
+ ctx = OpenSSL::SSL::SSLContext.new
+ ctx.ocsp_response_cb = proc { |ssl, res|
+ called = true
+ assert_nil res
+ true
+ }
+ server_connect(port, ctx) { }
+ end
+
+ assert_equal true, called
+ end
+
+ def test_ocsp_stapling_invalid_sign
+ issue_ocsp_signer
+ bres = OpenSSL::OCSP::BasicResponse.new
+ cid = OpenSSL::OCSP::CertificateId.new(@svr_cert, @ca_cert, "sha1")
+ bres.add_status(cid, OpenSSL::OCSP::V_CERTSTATUS_GOOD, 0, nil, -300, 500, [])
+ bres.sign(@ocsp_cert, @ocsp_key, [@ca_cert], 0)
+ res = OpenSSL::OCSP::Response.create(OpenSSL::OCSP::RESPONSE_STATUS_SUCCESSFUL, bres)
+
+ ctx_proc = proc { |ctx| ctx.ocsp_request_cb = proc { |ssl| res } }
+ start_server(ctx_proc: ctx_proc, ignore_listener_error: true) do |svr, port|
+ ctx = OpenSSL::SSL::SSLContext.new
+ ctx.ocsp_response_cb = proc { |ssl, res|
+ false
+ }
+ assert_raise_with_message(OpenSSL::SSL::SSLError, /invalid status response/) {
+ server_connect(port, ctx) { }
+ }
+ end
+ end
+
private
def start_server_version(version, ctx_proc = nil,
@@ -1314,4 +1386,13 @@ end
yield
}
end
+
+ def issue_ocsp_signer
+ @ocsp_key = Fixtures.pkey("dsa1024")
+ @ocsp = OpenSSL::X509::Name.parse("/DC=org/DC=ruby-lang/CN=OCSPSigner")
+ ocsp_exts = [
+ ["extendedKeyUsage", "OCSPSigning", true],
+ ]
+ @ocsp_cert = issue_cert(@ocsp, @ocsp_key, 4, ocsp_exts, @ca_cert, @ca_key)
+ end
end