From b2b6257c7acb8789c0c547f732792a035cf796ff Mon Sep 17 00:00:00 2001 From: Glenn Strauss Date: Tue, 9 Jun 2020 02:30:07 -0400 Subject: [PATCH] [mod_openssl] OCSP stapling (fixes #2469) Define ssl.stapling-file in lighttpd.conf in same scope as ssl.pemfile x-ref: "OCSP Stapling" https://redmine.lighttpd.net/issues/2469 --- src/mod_openssl.c | 433 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 427 insertions(+), 6 deletions(-) diff --git a/src/mod_openssl.c b/src/mod_openssl.c index e8dca1cc..8c8dff8d 100644 --- a/src/mod_openssl.c +++ b/src/mod_openssl.c @@ -6,8 +6,6 @@ * License: BSD 3-clause (same as lighttpd) */ /* - * future possible enhancements: OCSP stapling - * * Note: If session tickets are -not- disabled with * ssl.openssl.ssl-conf-cmd = ("Options" => "-SessionTicket") * mod_openssl rotates server ticket encryption key (STEK) every 8 hours @@ -34,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -71,6 +70,9 @@ #ifndef OPENSSL_NO_DH #include #endif +#ifndef OPENSSL_NO_OCSP +#include +#endif #if ! defined OPENSSL_NO_TLSEXT && ! defined SSL_CTRL_SET_TLSEXT_HOSTNAME #define OPENSSL_NO_TLSEXT @@ -113,8 +115,12 @@ typedef struct { EVP_PKEY *ssl_pemfile_pkey; X509 *ssl_pemfile_x509; STACK_OF(X509) *ssl_pemfile_chain; + buffer *ssl_stapling; const buffer *ssl_pemfile; const buffer *ssl_privkey; + const buffer *ssl_stapling_file; + time_t ssl_stapling_loadts; + time_t ssl_stapling_nextts; } plugin_cert; typedef struct { @@ -437,6 +443,49 @@ mod_openssl_session_ticket_key_check (const plugin_data *p, const time_t cur_ts) #endif /* TLSEXT_TYPE_session_ticket */ +#ifndef OPENSSL_NO_OCSP +#ifndef BORINGSSL_API_VERSION /* BoringSSL suggests using different API */ +static int +ssl_tlsext_status_cb(SSL *ssl, void *arg) +{ + #ifdef SSL_get_tlsext_status_type + if (TLSEXT_STATUSTYPE_ocsp != SSL_get_tlsext_status_type(ssl)) + return SSL_TLSEXT_ERR_NOACK; /* ignore if not client OCSP request */ + #endif + + handler_ctx *hctx = (handler_ctx *) SSL_get_app_data(ssl); + buffer *ssl_stapling = hctx->conf.pc->ssl_stapling; + if (NULL == ssl_stapling) return SSL_TLSEXT_ERR_NOACK; + UNUSED(arg); + + int len = (int)buffer_string_length(ssl_stapling); + + #ifdef WOLFSSL_VERSION /* WolfSSL does not require copy */ + uint8_t *ocsp_resp = (uint8_t *)ssl_stapling->ptr; + #else + /* OpenSSL and LibreSSL require copy (BoringSSL, too, if using compat API)*/ + uint8_t *ocsp_resp = OPENSSL_malloc(len); + if (NULL == ocsp_resp) + return SSL_TLSEXT_ERR_NOACK; /* ignore OCSP request if error occurs */ + memcpy(ocsp_resp, ssl_stapling->ptr, len); + #endif + + if (!SSL_set_tlsext_status_ocsp_resp(ssl, ocsp_resp, len)) { + log_error(hctx->r->conf.errh, __FILE__, __LINE__, + "SSL: failed to set OCSP response for TLS server name %s: %s", + hctx->r->uri.authority.ptr, ERR_error_string(ERR_get_error(), NULL)); + #ifndef WOLFSSL_VERSION /* WolfSSL does not require copy */ + OPENSSL_free(ocsp_resp); + #endif + return SSL_TLSEXT_ERR_NOACK; /* ignore OCSP request if error occurs */ + /*return SSL_TLSEXT_ERR_ALERT_FATAL;*/ + } + return SSL_TLSEXT_ERR_OK; +} +#endif +#endif + + INIT_FUNC(mod_openssl_init) { plugin_data_singleton = (plugin_data *)calloc(1, sizeof(plugin_data)); @@ -551,6 +600,7 @@ mod_openssl_free_config (server *srv, plugin_data * const p) EVP_PKEY_free(pc->ssl_pemfile_pkey); X509_free(pc->ssl_pemfile_x509); sk_X509_pop_free(pc->ssl_pemfile_chain, X509_free); + buffer_free(pc->ssl_stapling); } break; case 2: /* ssl.ca-file */ @@ -889,7 +939,9 @@ mod_openssl_merge_config_cpv (plugin_config * const pconf, const config_plugin_v case 12:/* ssl.acme-tls-1 */ pconf->ssl_acme_tls_1 = cpv->v.b; break; - case 13:/* debug.log-ssl-noise */ + case 13:/* ssl.stapling-file */ + break; + case 14:/* debug.log-ssl-noise */ pconf->ssl_log_noise = (0 != cpv->v.u); break; default:/* should not happen */ @@ -1120,6 +1172,20 @@ mod_openssl_cert_cb (SSL *ssl, void *arg) return 0; } + #ifndef OPENSSL_NO_OCSP + #ifdef BORINGSSL_API_VERSION + /* BoringSSL suggests API different than SSL_CTX_set_tlsext_status_cb() */ + buffer *ocsp_resp = pc->ssl_stapling; + if (NULL != ocsp_resp + && !SSL_set_ocsp_response(ssl, (uint8_t *)CONST_BUF_LEN(ocsp_resp))) { + log_error(hctx->r->conf.errh, __FILE__, __LINE__, + "SSL: failed to set OCSP response for TLS server name %s: %s", + hctx->r->uri.authority.ptr, ERR_error_string(ERR_get_error(), NULL)); + return 0; + } + #endif + #endif + if (hctx->conf.ssl_verifyclient) { if (NULL == hctx->conf.ssl_ca_file) { log_error(hctx->r->conf.errh, __FILE__, __LINE__, @@ -1303,8 +1369,327 @@ mod_openssl_evp_pkey_load_pem_file (const char *file, log_error_st *errh) } +#ifndef OPENSSL_NO_OCSP + +static buffer * +mod_openssl_load_stapling_file (const char *file, log_error_st *errh, buffer *b) +{ + /* load stapling .der into buffer *b only if successful + * + * Note: for some TLS libs, the OCSP stapling response is not copied when + * assigned to a session (and is reasonable since not changed frequently) + * - BoringSSL SSL_set_ocsp_response() + * - WolfSSL SSL_set_tlsext_status_ocsp_resp() (differs from OpenSSL API) + * Therefore, there is a potential race condition if the OCSP response is + * assigned to the session during the handshake and the Server Hello is + * partially sent, AND (unlikely, if possible at all), the TLS library is + * in the middle of reading this OSCP response buffer. If the OCSP response + * is replaced due to an updated ssl.stapling-file (checked periodically), + * AND the buffer is resized, this would be a problem. Resizing the buffer + * is unlikely since updated OSCP response for same certificate are + * typically the same size with the signature and dates refreshed. + */ + + #ifdef BORINGSSL_API_VERSION + + /* load raw .der file */ + /* (similar to mod_gnutls.c:mod_gnutls_load_file(), but some differences) */ + int fd = -1; + uint32_t sz = 0; + char *buf = NULL; + do { + fd = fdevent_open_cloexec(file,1,O_RDONLY,0); /*(1: follows symlinks)*/ + if (fd < 0) break; + + struct stat st; + if (0 != fstat(fd, &st)) break; + if (st.st_size == 0) break; + if (st.st_size >= UINT32_MAX) { /*(file too large for buffer uint32_t)*/ + errno = EOVERFLOW; + break; + } + + sz = (uint32_t)st.st_size; + buf = malloc(sz+1); /*(+1 trailing '\0')*/ + if (NULL == buf) break; + + ssize_t rd = 0; + unsigned int off = 0; + do { + rd = read(fd, buf+off, sz-off); + } while (rd > 0 ? (off += (unsigned int)rd) != sz : errno == EINTR); + if (off != sz) { /*(file truncated?)*/ + if (rd >= 0) errno = EIO; + break; + } + + if (NULL == b) b = buffer_init(); + buffer_copy_string_len(b, buf, sz); + memset(buf, 0, sz); + free(buf); + close(fd); + return b; + } while (0); + int errnum = errno; + log_perror(errh, __FILE__, __LINE__, "%s() %s", __func__, file); + if (fd >= 0) close(fd); + if (buf) { + memset(buf, 0, sz); + free(buf); + } + errno = errnum; + return NULL;; + + #else + + BIO *in = BIO_new_rdonly_file(file); + if (NULL == in) { + log_error(errh, __FILE__, __LINE__, + "SSL: BIO_new/BIO_read_filename('%s') failed", file); + return NULL; + } + + OCSP_RESPONSE *x = d2i_OCSP_RESPONSE_bio(in, NULL); + BIO_free(in); + if (NULL == x) { + log_error(errh, __FILE__, __LINE__, + "SSL: OCSP stapling file read error: %s %s", + ERR_error_string(ERR_get_error(), NULL), file); + return NULL; + } + + unsigned char *rspder = NULL; + int rspderlen = i2d_OCSP_RESPONSE(x, &rspder); + + if (rspderlen > 0) { + if (NULL == b) b = buffer_init(); + buffer_copy_string_len(b, (char *)rspder, (uint32_t)rspderlen); + } + + OPENSSL_free(rspder); + OCSP_RESPONSE_free(x); + return rspderlen ? b : NULL; + + #endif +} + + +static time_t +mod_openssl_asn1_time_to_posix (ASN1_TIME *asn1time) +{ + #ifdef LIBRESSL_VERSION_NUMBER + /* LibreSSL was forked from OpenSSL 1.0.1; does not have ASN1_TIME_diff */ + + /*(Note: all certificate times are expected to use UTC)*/ + /*(Note: does not strictly validate string contains appropriate digits)*/ + /*(Note: incorrectly assumes GMT if 'Z' or offset not provided)*/ + /*(Note: incorrectly ignores if local timezone might be in DST)*/ + + if (NULL == asn1time || NULL == asn1time->data) return (time_t)-1; + const char *s = (const char *)asn1time->data; + size_t len = strlen(s); + struct tm x; + x.tm_isdst = 0; + x.tm_yday = 0; + x.tm_wday = 0; + switch (asn1time->type) { + case V_ASN1_UTCTIME: /* 2-digit year */ + if (len < 8) return (time_t)-1; + len -= 8; + x.tm_year = (s[0]-'0')*10 + (s[1]-'0'); + x.tm_year += (x.tm_year < 50 ? 2000 : 1900); + s += 2; + break; + case V_ASN1_GENERALIZEDTIME: /* 4-digit year */ + if (len < 10) return (time_t)-1; + len -= 10; + x.tm_year = (s[0]-'0')*1000+(s[1]-'0')*100+(s[2]-'0')*10+(s[3]-'0'); + s += 4; + break; + default: + return (time_t)-1; + } + x.tm_mon = (s[0]-'0')*10 + (s[1]-'0'); + x.tm_mday = (s[2]-'0')*10 + (s[3]-'0'); + x.tm_hour = (s[4]-'0')*10 + (s[5]-'0'); + x.tm_min = 0; + x.tm_sec = 0; + s += 6; + if (len >= 2 && s[0] != '+' && s[0] != '-' && s[0] != 'Z') { + len -= 2; + x.tm_min = (s[0]-'0')*10 + (s[1]-'0'); + s += 2; + if (len >= 2 && s[0] != '+' && s[0] != '-' && s[0] != 'Z') { + len -= 2; + x.tm_sec = (s[0]-'0')*10 + (s[1]-'0'); + s += 2; + if (len && s[0] == '.') { + /*(ignore .fff fractional seconds; + * should be up to 3 digits but we ignore more)*/ + do { ++s; --len; } while (*s >= '0' && *s <= '9'); + } + } + } + int offset = 0; + if ((*s == '-' || *s == '+') && len != 5) { + offset = ((s[1]-'0')*10 + (s[2]-'0')) * 3600 + + ((s[3]-'0')*10 + (s[4]-'0')) * 60; + if (*s == '-') offset = -offset; + } + else if (s[0] != '\0' && (s[0] != 'Z' || s[1] != '\0')) + return (time_t)-1; + + if (x.tm_year == 9999 && x.tm_mon == 12 && x.tm_mday == 31 + && x.tm_hour == 23 && x.tm_min == 59 && x.tm_sec == 59 && s[0] == 'Z') + return (time_t)-1; // 99991231235959Z RFC 5280 + + #if 0 + #if defined(_WIN32) && !defined(__CYGWIN__) + #define timegm(x) _mkgmtime(x) + #endif + /* timegm() might not be available, and mktime() is sensitive to TZ */ + x.tm_year-= 1900; + x.tm_mon -= 1; + time_t t = timegm(&d); + return (t != (time_t)-1) ? t + offset : t; + #else + int y = x.tm_year; + int m = x.tm_mon; + int d = x.tm_mday; + /* days_from_civil() http://howardhinnant.github.io/date_algorithms.html */ + y -= m <= 2; + int era = y / 400; + int yoe = y - era * 400; // [0, 399] + int doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1; // [0, 365] + int doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; // [0, 146096] + int days_since_1970 = era * 146097 + doe - 719468; + return 60*(60*(24L*days_since_1970+x.tm_hour)+x.tm_min)+x.tm_sec+offset; + #endif + + #else + + /* Note: this does not check for integer overflow of time_t! */ + int day, sec; + return ASN1_TIME_diff(&day, &sec, NULL, asn1time) + ? log_epoch_secs + day*86400 + sec + : (time_t)-1; + + #endif +} + + +static time_t +mod_openssl_ocsp_next_update (plugin_cert *pc) +{ + #ifdef BORINGSSL_API_VERSION + UNUSED(pc); + return (time_t)-1; /*(not implemented)*/ + #else + buffer *der = pc->ssl_stapling; + const unsigned char *p = (unsigned char *)der->ptr; /*(p gets modified)*/ + OCSP_RESPONSE *ocsp = d2i_OCSP_RESPONSE(NULL,&p,buffer_string_length(der)); + if (NULL == ocsp) return (time_t)-1; + OCSP_BASICRESP *bs = OCSP_response_get1_basic(ocsp); + if (NULL == bs) { + OCSP_RESPONSE_free(ocsp); + return (time_t)-1; + } + + /* XXX: should save and evaluate cert status returned by these calls */ + ASN1_TIME *nextupd = NULL; + #ifdef WOLFSSL_VERSION /* WolfSSL limitation */ + /* WolfSSL does not provide OCSP_resp_get0() OCSP_single_get0_status() */ + OCSP_CERTID *id = (NULL != pc->ssl_pemfile_chain) + ? OCSP_cert_to_id(NULL, pc->ssl_pemfile_x509, + sk_X509_value(pc->ssl_pemfile_chain, 0)) + : NULL; + if (id == NULL) { + OCSP_BASICRESP_free(bs); + OCSP_RESPONSE_free(ocsp); + return (time_t)-1; + } + OCSP_resp_find_status(bs, id, NULL, NULL, NULL, NULL, &nextupd); + OCSP_CERTID_free(id); + #else + OCSP_single_get0_status(OCSP_resp_get0(bs, 0), NULL, NULL, NULL, &nextupd); + #endif + time_t t = nextupd ? mod_openssl_asn1_time_to_posix(nextupd) : (time_t)-1; + + /* Note: trust external process which creates ssl.stapling-file to verify + * (as well as to validate certificate status) + * future: verify OCSP response here to double-check */ + + OCSP_BASICRESP_free(bs); + OCSP_RESPONSE_free(ocsp); + + return t; + #endif +} + + +static int +mod_openssl_reload_stapling_file (server *srv, plugin_cert *pc, const time_t cur_ts) +{ + buffer *b = mod_openssl_load_stapling_file(pc->ssl_stapling_file->ptr, + srv->errh, pc->ssl_stapling); + if (!b) return 0; + + pc->ssl_stapling = b; /*(unchanged unless orig was NULL)*/ + pc->ssl_stapling_loadts = cur_ts; + pc->ssl_stapling_nextts = mod_openssl_ocsp_next_update(pc); + if (pc->ssl_stapling_nextts == (time_t)-1) { + /* "Next Update" might not be provided by OCSP responder + * Use 3600 sec (1 hour) in that case. */ + /* retry in 1 hour if unable to determine Next Update */ + pc->ssl_stapling_nextts = cur_ts + 3600; + pc->ssl_stapling_loadts = 0; + } + + return 1; +} + + +static int +mod_openssl_refresh_stapling_file (server *srv, plugin_cert *pc, const time_t cur_ts) +{ + if (pc->ssl_stapling && pc->ssl_stapling_nextts - 256 > cur_ts) + return 1; /* skip check for refresh unless close to expire */ + struct stat st; + if (0 != stat(pc->ssl_stapling_file->ptr, &st) + || st.st_mtime <= pc->ssl_stapling_loadts) { + if (pc->ssl_stapling_nextts < cur_ts) { + /* discard expired OCSP stapling response */ + buffer_free(pc->ssl_stapling); + pc->ssl_stapling = NULL; + } + return 1; + } + return mod_openssl_reload_stapling_file(srv, pc, cur_ts); +} + + +static void +mod_openssl_refresh_stapling_files (server *srv, const plugin_data *p, const time_t cur_ts) +{ + /* future: might construct array of (plugin_cert *) at startup + * to avoid the need to search for them here */ + for (int i = 0, used = p->nconfig; i < used; ++i) { + const config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0]; + for (; cpv->k_id != -1; ++cpv) { + if (cpv->k_id != 0) continue; /* k_id == 0 for ssl.pemfile */ + if (cpv->vtype != T_CONFIG_LOCAL) continue; + plugin_cert *pc = cpv->v.v; + if (!buffer_string_is_empty(pc->ssl_stapling_file)) + mod_openssl_refresh_stapling_file(srv, pc, cur_ts); + } + } +} + +#endif + + static plugin_cert * -network_openssl_load_pemfile (server *srv, const buffer *pemfile, const buffer *privkey) +network_openssl_load_pemfile (server *srv, const buffer *pemfile, const buffer *privkey, const buffer *ssl_stapling_file) { if (!mod_openssl_init_once_openssl(srv)) return NULL; @@ -1340,6 +1725,23 @@ network_openssl_load_pemfile (server *srv, const buffer *pemfile, const buffer * pc->ssl_pemfile_chain= ssl_pemfile_chain; pc->ssl_pemfile = pemfile; pc->ssl_privkey = privkey; + pc->ssl_stapling = NULL; + pc->ssl_stapling_file= ssl_stapling_file; + pc->ssl_stapling_loadts = 0; + pc->ssl_stapling_nextts = 0; + + if (!buffer_string_is_empty(pc->ssl_stapling_file)) { + #ifndef OPENSSL_NO_OCSP + if (!mod_openssl_reload_stapling_file(srv, pc, log_epoch_secs)) { + /* continue without OCSP response if there is an error */ + } + #else + log_error(srv->errh, __FILE__, __LINE__, "SSL:" + "OCSP stapling not supported; ignoring %s", + pc->ssl_stapling_file->ptr); + #endif + } + return pc; } @@ -1858,6 +2260,12 @@ network_init_ssl (server *srv, plugin_config_socket *s, plugin_data *p) SSL_CTX_set_tlsext_ticket_key_cb(s->ssl_ctx, ssl_tlsext_ticket_key_cb); #endif + #ifndef OPENSSL_NO_OCSP + #ifndef BORINGSSL_API_VERSION /* BoringSSL suggests using different API */ + SSL_CTX_set_tlsext_status_cb(s->ssl_ctx, ssl_tlsext_status_cb); + #endif + #endif + #if OPENSSL_VERSION_NUMBER >= 0x10002000 \ && !defined(LIBRESSL_VERSION_NUMBER) @@ -2259,6 +2667,9 @@ SETDEFAULTS_FUNC(mod_openssl_set_defaults) ,{ CONST_STR_LEN("ssl.acme-tls-1"), T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION } + ,{ CONST_STR_LEN("ssl.stapling-file"), + T_CONFIG_STRING, + T_CONFIG_SCOPE_CONNECTION } ,{ CONST_STR_LEN("debug.log-ssl-noise"), T_CONFIG_BOOL, T_CONFIG_SCOPE_CONNECTION } @@ -2281,6 +2692,7 @@ SETDEFAULTS_FUNC(mod_openssl_set_defaults) config_plugin_value_t *cpv = p->cvlist + p->cvlist[i].v.u2[0]; config_plugin_value_t *pemfile = NULL; config_plugin_value_t *privkey = NULL; + const buffer *ssl_stapling_file = NULL; const buffer *ssl_ca_file = NULL; const buffer *ssl_ca_dn_file = NULL; const buffer *ssl_ca_crl_file = NULL; @@ -2345,7 +2757,11 @@ SETDEFAULTS_FUNC(mod_openssl_set_defaults) case 10:/* ssl.verifyclient.username */ case 11:/* ssl.verifyclient.exportcert */ case 12:/* ssl.acme-tls-1 */ - case 13:/* debug.log-ssl-noise */ + break; + case 13:/* ssl.stapling-file */ + ssl_stapling_file = cpv->v.b; + break; + case 14:/* debug.log-ssl-noise */ break; default:/* should not happen */ break; @@ -2408,7 +2824,8 @@ SETDEFAULTS_FUNC(mod_openssl_set_defaults) #endif if (NULL == privkey) privkey = pemfile; pemfile->v.v = - network_openssl_load_pemfile(srv, pemfile->v.b, privkey->v.b); + network_openssl_load_pemfile(srv, pemfile->v.b, privkey->v.b, + ssl_stapling_file); if (pemfile->v.v) pemfile->vtype = T_CONFIG_LOCAL; else @@ -3174,6 +3591,10 @@ TRIGGER_FUNC(mod_openssl_handle_trigger) { mod_openssl_session_ticket_key_check(p, cur_ts); #endif + #ifndef OPENSSL_NO_OCSP + mod_openssl_refresh_stapling_files(srv, p, cur_ts); + #endif + return HANDLER_GO_ON; }