Browse Source

[mod_gnutls] add basic OCSP response stapling support

Change-Id: I700b2afd0e0fc60ce4f864e77166e3fa2e36aaae
master
Stefan Bühler 4 years ago
parent
commit
d72a3c2940
6 changed files with 394 additions and 10 deletions
  1. +45
    -2
      doc/mod_gnutls.xml
  2. +1
    -1
      src/CMakeLists.txt
  3. +2
    -2
      src/modules/Makefile.am
  4. +288
    -0
      src/modules/gnutls_ocsp.c
  5. +25
    -0
      src/modules/gnutls_ocsp.h
  6. +33
    -5
      src/modules/mod_gnutls.c

+ 45
- 2
doc/mod_gnutls.xml View File

@@ -10,8 +10,8 @@
<short>(mandatory) the socket address to listen on (same as "listen":plugin_core.html#plugin_core__setup_listen), can be specified more than once to setup multiple sockets with the same options</short>
</entry>
<entry name="pemfile">
<short>(mandatory) file containing the private key, certificate and intermediate certificates (the root certificate is usually not
included); alternatively it can be a key-value list with a "key" and a "cert" entry.</short>
<short>(mandatory) file containing the private key, certificate, intermediate certificates (the root certificate is usually not
included) and an OCSP response; alternatively it can be a key-value list with a "key" and a "cert" entry, and optionally a "ocsp" entry.</short>
</entry>
<entry name="pin">
<short>the PIN (or password) to use when using PKCS #11 modules or encrypted keys. The pin is kept in memory.</short>
@@ -123,6 +123,24 @@
}
</config>
</example>

<example title="Simple TLS on IPv4 and IPv6 with OCSP stapling">
<config>
setup {
module_load "mod_gnutls";
gnutls (
"priority" => "PFS:-3DES-CBC:-ARCFOUR-128:-VERS-SSL3.0:-SHA1:+SHA1:+RSA:%SERVER_PRECEDENCE",
"listen" => "0.0.0.0:443",
"listen" => "[::]:443",
"pemfile" => (
"key" => "/etc/certs/lighttpd.pem",
"cert" => "/etc/certs/lighttpd.pem",
"ocsp" => "/etc/certs/lighttpd-ocsp.der",
)
);
}
</config>
</example>
</setup>

<section title="Server Name Indication (SNI)">
@@ -165,6 +183,31 @@
</textile>
</section>

<section title="OCSP stapling">
<textile>
"OCSP stapling":https://en.wikipedia.org/wiki/OCSP_stapling is used to assure a client the certificate is still valid (i.e. not revoked); you can put an OCSP response into the certificate file in an "OCSP RESPNSE"-PEM block (there is probably no standard for this, juse base64-encode the DER-response you have), or specify the (DER or PEM formatted) OCSP response as separate file using "ocsp" in a "pemfile" block.

Server Name Indication (SNI) should work fine, as an OCSP response is only used if it matches the certificate in use for the connection.

The fetch backends do NOT support OCSP stapling yet (in the future they should support the "OCSP RESPNSE"-PEM block).

Lighttpd does NOT automatically reload OCSP responses; you have to restart to load new OCSP responses (a cron job is probably the right way to do it).

If you have your certificate and the issuer-certificate (the one that signed yours) in separate files you can request an OCSP response like that (using the GnuTLS "certtool"):

<pre>
certtool --ask --load-issuer issuer.pem --load-cert cert.pem --outfile ocsp.der
</pre>

Converting into PEM format can be done like this:

<pre>
echo "-----BEGIN OCSP RESPONSE-----"; base64 --wrap=64 ocsp.der; echo "-----END OCSP RESPONSE-----"
</pre>

</textile>
</section>

<section title="protect-against-beast">
<textile>
"BEAST":http://en.wikipedia.org/wiki/Transport_Layer_Security#BEAST_attack is considered mitigated on clients by many now, so this workaround is no longer recommended and disabled by default.


+ 1
- 1
src/CMakeLists.txt View File

@@ -384,7 +384,7 @@ IF(WITH_LUA)
ENDIF(WITH_LUA)

IF(WITH_GNUTLS)
ADD_AND_INSTALL_LIBRARY(mod_gnutls "modules/mod_gnutls.c;modules/gnutls_filter.c")
ADD_AND_INSTALL_LIBRARY(mod_gnutls "modules/mod_gnutls.c;modules/gnutls_filter.c;modules/gnutls_ocsp.c")
TARGET_LINK_LIBRARIES(mod_gnutls ${GNUTLS_LDFLAGS} ${IDN_LDFLAGS})
ADD_TARGET_PROPERTIES(mod_gnutls COMPILE_FLAGS ${GNUTLS_CFLAGS} ${IDN_CFLAGS})
ENDIF(WITH_GNUTLS)


+ 2
- 2
src/modules/Makefile.am View File

@@ -77,11 +77,11 @@ libmod_fortune_la_LIBADD = $(common_libadd)
if USE_GNUTLS
install_libs += libmod_gnutls.la
libmod_gnutls_la_CPPFLAGS = $(AM_CPPFLAGS) $(GNUTLS_CFLAGS) $(IDN_FLAGS)
libmod_gnutls_la_SOURCES = mod_gnutls.c gnutls_filter.c
libmod_gnutls_la_SOURCES = mod_gnutls.c gnutls_filter.c gnutls_ocsp.c
libmod_gnutls_la_LDFLAGS = $(common_ldflags)
libmod_gnutls_la_LIBADD = $(common_libadd) $(GNUTLS_LIBS) $(IDN_LIBS)
endif
EXTRA_DIST += gnutls_filter.h
EXTRA_DIST += gnutls_filter.h gnutls_ocsp.h

install_libs += libmod_limit.la
libmod_limit_la_SOURCES = mod_limit.c


+ 288
- 0
src/modules/gnutls_ocsp.c View File

@@ -0,0 +1,288 @@

#include "gnutls_ocsp.h"

#include <gnutls/ocsp.h>
#include <gnutls/crypto.h>

typedef struct ocsp_response_cert_entry {
gnutls_digest_algorithm_t digest;
gnutls_datum_t issuer_name_hash;
gnutls_datum_t serial;
} ocsp_response_cert_entry;

typedef struct ocsp_response {
gnutls_ocsp_resp_t resp;
gnutls_datum_t resp_data; /* DER encoded */

GArray* certificates;
} ocsp_response;

struct liGnuTLSOCSP {
GArray* responses;
size_t max_serial_length;
size_t max_hash_length;
};

static void clear_entry(ocsp_response_cert_entry* entry) {
gnutls_free(entry->issuer_name_hash.data);
gnutls_free(entry->serial.data);
memset(entry, 0, sizeof(*entry));
}

static int get_entry(liServer *srv, ocsp_response_cert_entry* entry, gnutls_ocsp_resp_t resp, unsigned int ndx) {
int r;
memset(entry, 0, sizeof(*entry));

r = gnutls_ocsp_resp_get_single(
resp, ndx, &entry->digest, &entry->issuer_name_hash, NULL, &entry->serial,
NULL, NULL, NULL, NULL, NULL);

if (GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE == r) return r;

if (GNUTLS_E_SUCCESS != r) {
ERROR(srv, "Couldn't retrieve OCSP response information for entry %u (%s): %s",
ndx,
gnutls_strerror_name(r), gnutls_strerror(r));
return r;
}

if (0 == entry->serial.size || GNUTLS_DIG_UNKNOWN == entry->digest || entry->issuer_name_hash.size != gnutls_hash_get_len(entry->digest)) {
ERROR(srv, "Invalid OCSP response data in entry %u", ndx);
return GNUTLS_E_OCSP_RESPONSE_ERROR;
}

return GNUTLS_E_SUCCESS;
}

static void clear_response(ocsp_response* response) {
gnutls_free(response->resp_data.data);
gnutls_ocsp_resp_deinit(response->resp);
if (NULL != response->certificates) {
guint i;
for (i = 0; i < response->certificates->len; ++i) {
ocsp_response_cert_entry* entry = &g_array_index(response->certificates, ocsp_response_cert_entry, i);
clear_entry(entry);
}
g_array_free(response->certificates, TRUE);
}
memset(response, 0, sizeof(*response));
}

/* pass ownership of der_data */
static gboolean add_response(liServer *srv, liGnuTLSOCSP *ocsp, gnutls_datum_t* der_data) {
ocsp_response response;
int r;
guint i;

memset(&response, 0, sizeof(response));

response.resp_data = *der_data;
der_data->data = NULL; der_data->size = 0;

if (GNUTLS_E_SUCCESS != (r = gnutls_ocsp_resp_init(&response.resp))) {
ERROR(srv, "gnutls_ocsp_resp_init (%s): %s",
gnutls_strerror_name(r), gnutls_strerror(r));
goto error;
}

if (GNUTLS_E_SUCCESS != (r = gnutls_ocsp_resp_import(response.resp, &response.resp_data))) {
ERROR(srv, "gnutls_ocsp_resp_import (%s): %s",
gnutls_strerror_name(r), gnutls_strerror(r));
goto error;
}

response.certificates = g_array_new(FALSE, TRUE, sizeof(ocsp_response_cert_entry));
for (i = 0; ; ++i) {
ocsp_response_cert_entry entry;

r = get_entry(srv, &entry, response.resp, i);
if (GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE == r) break; /* got them all */
if (GNUTLS_E_SUCCESS != r) goto error;

g_array_append_vals(response.certificates, &entry, 1);

if (entry.serial.size > ocsp->max_serial_length) ocsp->max_serial_length = entry.serial.size;
if (entry.issuer_name_hash.size > ocsp->max_hash_length) ocsp->max_hash_length = entry.issuer_name_hash.size;
}

g_array_append_vals(ocsp->responses, &response, 1);
return TRUE;

error:
clear_response(&response);
return FALSE;
}

static int ctx_ocsp_response(gnutls_session_t session, void* ptr, gnutls_datum_t* ocsp_resp) {
liGnuTLSOCSP* ocsp = ptr;
guint i;
int r;
gnutls_datum_t serial = { NULL, 0 };
gnutls_datum_t issuer_name = { NULL, 0 };
char* issuer_name_hash = NULL;

if (0 == ocsp->responses->len) return GNUTLS_E_NO_CERTIFICATE_STATUS;

serial.size = ocsp->max_serial_length;
serial.data = gnutls_malloc(serial.size);
issuer_name_hash = gnutls_malloc(ocsp->max_hash_length);

{
gnutls_datum_t const* crt_datum;
gnutls_x509_crt_t crt = NULL;
size_t serial_size = ocsp->max_serial_length;

crt_datum = gnutls_certificate_get_ours(session); /* memory is NOT owned */
if (GNUTLS_E_SUCCESS != (r = gnutls_x509_crt_init(&crt))) goto cleanup;
if (GNUTLS_E_SUCCESS != (r = gnutls_x509_crt_import(crt, crt_datum, GNUTLS_X509_FMT_DER))) {
gnutls_x509_crt_deinit(crt);
goto cleanup;
}

;
if (GNUTLS_E_SUCCESS != (r = gnutls_x509_crt_get_serial(crt, serial.data, &serial_size))) {
gnutls_x509_crt_deinit(crt);
goto cleanup;
}
serial.size = serial_size;

if (GNUTLS_E_SUCCESS != (r = gnutls_x509_crt_get_raw_issuer_dn(crt, &issuer_name))) {
gnutls_x509_crt_deinit(crt);
goto cleanup;
}

gnutls_x509_crt_deinit(crt);
}

for (i = 0; i < ocsp->responses->len; ++i) {
ocsp_response* response = &g_array_index(ocsp->responses, ocsp_response, i);
guint j;

for (j = 0; j < response->certificates->len; ++j) {
ocsp_response_cert_entry* entry = &g_array_index(response->certificates, ocsp_response_cert_entry, i);

if (serial.size != entry->serial.size
|| 0 != memcmp(serial.data, entry->serial.data, serial.size)) continue;

if (GNUTLS_E_SUCCESS != (r = gnutls_hash_fast(entry->digest, issuer_name.data, issuer_name.size, issuer_name_hash))) goto cleanup;

if (0 != memcmp(issuer_name_hash, entry->issuer_name_hash.data, entry->issuer_name_hash.size)) continue;

ocsp_resp->size = response->resp_data.size;
ocsp_resp->data = gnutls_malloc(ocsp_resp->size);
memcpy(ocsp_resp->data, response->resp_data.data, ocsp_resp->size);
r = GNUTLS_E_SUCCESS;
goto cleanup;
}
}

r = GNUTLS_E_NO_CERTIFICATE_STATUS;

cleanup:
gnutls_free(issuer_name_hash);
gnutls_free(issuer_name.data);
gnutls_free(serial.data);

return r;
}

liGnuTLSOCSP* li_gnutls_ocsp_new(void) {
liGnuTLSOCSP* ocsp = g_slice_new0(liGnuTLSOCSP);
ocsp->responses = g_array_new(FALSE, TRUE, sizeof(ocsp_response));
ocsp->max_hash_length = ocsp->max_serial_length = 0;
return ocsp;
}

void li_gnutls_ocsp_free(liGnuTLSOCSP *ocsp) {
if (NULL != ocsp->responses) {
guint i;
for (i = 0; i < ocsp->responses->len; ++i) {
ocsp_response* response = &g_array_index(ocsp->responses, ocsp_response, i);
clear_response(response);
}
g_array_free(ocsp->responses, TRUE);
}
memset(ocsp, 0, sizeof(*ocsp));
g_slice_free(liGnuTLSOCSP, ocsp);
}

void li_gnutls_ocsp_use(liGnuTLSOCSP *ocsp, gnutls_certificate_credentials_t creds) {
gnutls_certificate_set_ocsp_status_request_function(creds, ctx_ocsp_response, ocsp);
}

gboolean li_gnutls_ocsp_add(liServer *srv, liGnuTLSOCSP *ocsp, const char* filename) {
int r;
gnutls_datum_t file = { NULL, 0 };
gnutls_datum_t decoded = { NULL, 0 };
gnutls_datum_t* der_data;
gboolean result = FALSE;

if (GNUTLS_E_SUCCESS != (r = gnutls_load_file(filename, &file))) {
ERROR(srv, "Failed to load OCSP file '%s' (%s): %s",
filename,
gnutls_strerror_name(r), gnutls_strerror(r));
goto error;
}

/* decode pem "-----BEGIN OCSP RESPONSE-----", otherwise expect DER */
if (file.size > 20 && 0 == memcmp(file.data, CONST_STR_LEN("-----BEGIN "))) {
r = gnutls_pem_base64_decode2("OCSP RESPONSE", &file, &decoded);

if (GNUTLS_E_SUCCESS != r) {
ERROR(srv, "gnutls_pem_base64_decode2 failed to decode OCSP RESPONSE from '%s' (%s): %s",
filename,
gnutls_strerror_name(r), gnutls_strerror(r));
goto error;
}
der_data = &decoded;
} else {
der_data = &file;
}

result = add_response(srv, ocsp, der_data);
if (!result) {
ERROR(srv, "Failed loading OCSP response from '%s'", filename);
}

error:
gnutls_free(file.data);
gnutls_free(decoded.data);
return result;
}

gboolean li_gnutls_ocsp_search(liServer *srv, liGnuTLSOCSP *ocsp, const char* filename) {
int r;
gnutls_datum_t file = { NULL, 0 };
gnutls_datum_t decoded = { NULL, 0 };
gboolean result = FALSE;

if (GNUTLS_E_SUCCESS != (r = gnutls_load_file(filename, &file))) {
ERROR(srv, "Failed to load OCSP file '%s' (%s): %s",
filename,
gnutls_strerror_name(r), gnutls_strerror(r));
goto error;
}

r = gnutls_pem_base64_decode2("OCSP RESPONSE", &file, &decoded);

if (GNUTLS_E_SUCCESS == r) {
result = add_response(srv, ocsp, &decoded);
if (!result) {
ERROR(srv, "Failed loading OCSP response from '%s'", filename);
goto error;
}
} else if (GNUTLS_E_BASE64_UNEXPECTED_HEADER_ERROR == r) {
/* ignore GNUTLS_E_BASE64_UNEXPECTED_HEADER_ERROR */
} else {
ERROR(srv, "gnutls_pem_base64_decode2 failed to decode OCSP RESPONSE from '%s' (%s): %s",
filename,
gnutls_strerror_name(r), gnutls_strerror(r));
/* continue anyway */
}
result = TRUE;

error:
gnutls_free(file.data);
gnutls_free(decoded.data);
return result;
}

+ 25
- 0
src/modules/gnutls_ocsp.h View File

@@ -0,0 +1,25 @@
#ifndef _LIGHTTPD_GNUTLS_OCSP_H_
#define _LIGHTTPD_GNUTLS_OCSP_H_

#include <lighttpd/base.h>

#include <gnutls/gnutls.h>

typedef struct liGnuTLSOCSP liGnuTLSOCSP;

LI_API liGnuTLSOCSP* li_gnutls_ocsp_new(void);

/* doesn't call closed_cb; but you can call this from closed_cb */
LI_API void li_gnutls_ocsp_free(liGnuTLSOCSP *ocsp);

LI_API void li_gnutls_ocsp_use(liGnuTLSOCSP *ocsp, gnutls_certificate_credentials_t creds);

/* load DER or PEM ("OCSP RESPONSE") encoded ocsp response */
LI_API gboolean li_gnutls_ocsp_add(liServer *srv, liGnuTLSOCSP *ocsp, const char* filename);

/* search in PEM file for a OCSP RESPONSE block and add it if there is one;
* returns only FALSE if a block was found which COULDN'T be loaded
*/
LI_API gboolean li_gnutls_ocsp_search(liServer *srv, liGnuTLSOCSP *ocsp, const char* filename);

#endif

+ 33
- 5
src/modules/mod_gnutls.c View File

@@ -16,6 +16,7 @@
#include <lighttpd/throttle.h>

#include "gnutls_filter.h"
#include "gnutls_ocsp.h"
#include "ssl_client_hello_parser.h"
#include "ssl-session-db.h"

@@ -70,6 +71,8 @@ struct mod_context {
gnutls_datum_t ticket_key;
#endif

liGnuTLSOCSP* ocsp;

#ifdef USE_SNI
liFetchDatabase *sni_db, *sni_backend_db;
gnutls_certificate_credentials_t sni_fallback_cert;
@@ -237,6 +240,8 @@ static void mod_gnutls_context_release(mod_context *ctx) {
ctx->pin = NULL;
}

li_gnutls_ocsp_free(ctx->ocsp);

g_slice_free(mod_context, ctx);
}
}
@@ -283,6 +288,8 @@ static mod_context *mod_gnutls_context_new(liServer *srv) {
}
#endif

ctx->ocsp = li_gnutls_ocsp_new();

ctx->refcount = 1;
ctx->protect_against_beast = 1;

@@ -625,15 +632,16 @@ static void gnutls_setup_listen_cb(liServer *srv, int fd, gpointer data) {
srv_sock->release_cb = mod_gnutls_sock_release;
}

static gboolean creds_add_pemfile(liServer *srv, gnutls_certificate_credentials_t creds, liValue *pemfile) {
static gboolean creds_add_pemfile(liServer *srv, mod_context *ctx, gnutls_certificate_credentials_t creds, liValue *pemfile) {
const char *keyfile = NULL;
const char *certfile = NULL;
const char *ocspfile = NULL;
int r;

if (LI_VALUE_STRING == li_value_type(pemfile)) {
keyfile = pemfile->data.string->str;
certfile = pemfile->data.string->str;
} else if (li_value_list_has_len(pemfile, 2)) {
} else if (li_value_list_len(pemfile) >= 2) {
if (NULL == (pemfile = li_value_to_key_value_list(pemfile))) {
ERROR(srv, "%s", "gnutls expects a hash/key-value list or a string as pemfile parameter");
return FALSE;
@@ -670,13 +678,23 @@ static gboolean creds_add_pemfile(liServer *srv, gnutls_certificate_credentials_
return FALSE;
}
certfile = entryValue->data.string->str;
} else if (g_str_equal(entryKeyStr->str, "ocsp")) {
if (LI_VALUE_STRING != li_value_type(entryValue)) {
ERROR(srv, "%s", "gnutls pemfile.ocsp expects a string as parameter");
return FALSE;
}
if (NULL != ocspfile) {
ERROR(srv, "gnutls unexpected duplicate parameter pemfile %s", entryKeyStr->str);
return FALSE;
}
ocspfile = entryValue->data.string->str;
} else {
ERROR(srv, "invalid parameter for gnutls: pemfile %s", entryKeyStr->str);
return FALSE;
}
LI_VALUE_END_FOREACH()
} else {
ERROR(srv, "%s", "gnutls expects a hash/key-value list or a string as pemfile parameter");
ERROR(srv, "%s", "gnutls expects a hash/key-value list (with at least \"key\" and \"cert\" entries) or a string as pemfile parameter");
return FALSE;
}

@@ -692,6 +710,12 @@ static gboolean creds_add_pemfile(liServer *srv, gnutls_certificate_credentials_
return FALSE;
}

if (NULL != ocspfile) {
if (!li_gnutls_ocsp_add(srv, ctx->ocsp, ocspfile)) return FALSE;
} else {
if (!li_gnutls_ocsp_search(srv, ctx->ocsp, certfile)) return FALSE;
}

return TRUE;
}

@@ -852,6 +876,8 @@ static gboolean gnutls_setup(liServer *srv, liPlugin* p, liValue *val, gpointer
gnutls_certificate_set_pin_function(ctx->server_cert, pin_callback, ctx->pin);
#endif

li_gnutls_ocsp_use(ctx->ocsp, ctx->server_cert);

#ifdef USE_SNI
if (NULL != sni_backend) {
liFetchDatabase *backend = li_server_get_fetch_database(srv, sni_backend);
@@ -875,7 +901,9 @@ static gboolean gnutls_setup(liServer *srv, liPlugin* p, liValue *val, gpointer
gnutls_certificate_set_pin_function(ctx->sni_fallback_cert, pin_callback, ctx->pin);
#endif

if (!creds_add_pemfile(srv, ctx->sni_fallback_cert, sni_fallback_pemfile)) {
li_gnutls_ocsp_use(ctx->ocsp, ctx->sni_fallback_cert);

if (!creds_add_pemfile(srv, ctx, ctx->sni_fallback_cert, sni_fallback_pemfile)) {
goto error_free_ctx;
}
}
@@ -891,7 +919,7 @@ static gboolean gnutls_setup(liServer *srv, liPlugin* p, liValue *val, gpointer

if (g_str_equal(entryKeyStr->str, "pemfile")) {

if (!creds_add_pemfile(srv, ctx->server_cert, entryValue)) {
if (!creds_add_pemfile(srv, ctx, ctx->server_cert, entryValue)) {
goto error_free_ctx;
}
}


Loading…
Cancel
Save