diff --git a/doc/configuration.txt b/doc/configuration.txt index 370fd53e5..6d215eb5b 100644 --- a/doc/configuration.txt +++ b/doc/configuration.txt @@ -8264,7 +8264,8 @@ crt are loaded. If a directory name is used instead of a PEM file, then all files found in - that directory will be loaded. This directive may be specified multiple times + that directory will be loaded unless their name ends with '.issuer' or + '.ocsp' (reserved extensions). This directive may be specified multiple times in order to load certificates from multiple files or directories. The certificates will be presented to clients who provide a valid TLS Server Name Indication field matching one of their CN or alt subjects. Wildcards are @@ -8287,6 +8288,20 @@ crt others, e.g. nginx, result in a wrong bundle that will not work for some clients). + For each PEM file, haproxy checks for the presence of file at the same path + suffixed by ".ocsp". If such file is found, support for the TLS Certificate + Status Request extension (also known as "OCSP stapling") is automatically + enabled. The content of this file is optional. If not empty, it must contain + a valid OCSP Response in DER format. In order to be valid an OCSP Response + must comply with the following rules: it has to indicate a good status, + it has to be a single response for the certificate of the PEM file, and it + has to be valid at the moment of addition. If these rules are not respected + the OCSP Response is ignored and a warning is emitted. In order to identify + which certificate an OCSP Response applies to, the issuer's certificate is + necessary. If the issuer's certificate is not found in the PEM file, it will + be loaded from a file at the same path as the PEM file suffixed by ".issuer" + if it exists otherwise it will fail with an error. + crt-ignore-err This setting is only available when support for OpenSSL was built in. Sets a comma separated list of errorIDs to ignore during verify at depth == 0. If @@ -13514,6 +13529,18 @@ set server / weight [%] Change a server's weight to the value passed in argument. This is the exact equivalent of the "set weight" command below. +set ssl ocsp-response + This command is used to update an OCSP Response for a certificate (see "crt" + on "bind" lines). Same controls are performed as during the initial loading of + the response. The must be passed as a base64 encoded string of the + DER encoded response from the OCSP server. + + Example: + openssl ocsp -issuer issuer.pem -cert server.pem \ + -host ocsp.issuer.com:80 -respout resp.der + echo "set ssl ocsp-response $(base64 -w 10000 resp.der)" | \ + socat stdio /var/run/haproxy.stat + set table key [data. ]* Create or update a stick-table entry in the table. If the key is not present, an entry is inserted. See stick-table in section 4.2 to find all possible diff --git a/include/common/defaults.h b/include/common/defaults.h index 8d5d62a6f..0d18281ba 100644 --- a/include/common/defaults.h +++ b/include/common/defaults.h @@ -230,4 +230,9 @@ #define TIME_STATS_SAMPLES 512 #endif +/* max ocsp cert id asn1 encoded length */ +#ifndef OCSP_MAX_CERTID_ASN1_LENGTH +#define OCSP_MAX_CERTID_ASN1_LENGTH 128 +#endif + #endif /* _COMMON_DEFAULTS_H */ diff --git a/include/proto/ssl_sock.h b/include/proto/ssl_sock.h index 2d1dadc6d..0902fde98 100644 --- a/include/proto/ssl_sock.h +++ b/include/proto/ssl_sock.h @@ -54,6 +54,9 @@ char *ssl_sock_get_version(struct connection *conn); int ssl_sock_get_cert_used(struct connection *conn); char *ssl_sock_get_common_name(struct connection *conn); unsigned int ssl_sock_get_verify_result(struct connection *conn); +#ifdef SSL_CTRL_SET_TLSEXT_STATUS_REQ_CB +int ssl_sock_update_ocsp_response(struct chunk *ocsp_response, char **err); +#endif #endif /* _PROTO_SSL_SOCK_H */ diff --git a/src/dumpstats.c b/src/dumpstats.c index bc0bea7d0..36b468406 100644 --- a/src/dumpstats.c +++ b/src/dumpstats.c @@ -35,6 +35,7 @@ #include #include #include +#include #include @@ -195,6 +196,7 @@ static const char stats_sock_usage_msg[] = " add map : add map entry\n" " del map : delete map entry\n" " clear map : clear the content of this map\n" + " set ssl : set statement for ssl\n" ""; static const char stats_permission_denied_msg[] = @@ -1789,6 +1791,50 @@ static int stats_sock_parse_request(struct stream_interface *si, char *line) appctx->st0 = STAT_CLI_PRINT; return 1; } +#ifdef USE_OPENSSL + else if (strcmp(args[1], "ssl") == 0) { + if (strcmp(args[2], "ocsp-response") == 0) { +#ifdef SSL_CTRL_SET_TLSEXT_STATUS_REQ_CB + char *err = NULL; + + /* Expect two parameters: certificate file name and the new response in base64 encoding */ + if (!*args[3]) { + appctx->ctx.cli.msg = "'set ssl ocsp-response' expects response in base64 encoding.\n"; + appctx->st0 = STAT_CLI_PRINT; + return 1; + } + + trash.len = base64dec(args[3], strlen(args[3]), trash.str, trash.size); + if (trash.len < 0) { + appctx->ctx.cli.msg = "'set ssl ocsp-response' received invalid base64 encoded response.\n"; + appctx->st0 = STAT_CLI_PRINT; + return 1; + } + + if (ssl_sock_update_ocsp_response(&trash, &err)) { + if (err) { + memprintf(&err, "%s.\n", err); + appctx->ctx.cli.err = err; + appctx->st0 = STAT_CLI_PRINT_FREE; + } + return 1; + } + appctx->ctx.cli.msg = "OCSP Response updated!"; + appctx->st0 = STAT_CLI_PRINT; + return 1; +#else + appctx->ctx.cli.msg = "HAProxy was compiled against a version of OpenSSL that doesn't support OCSP stapling.\n"; + appctx->st0 = STAT_CLI_PRINT; + return 1; +#endif + } + else { + appctx->ctx.cli.msg = "'set ssl' only supports 'ocsp-response'.\n"; + appctx->st0 = STAT_CLI_PRINT; + return 1; + } + } +#endif else { /* unknown "set" parameter */ return 0; } diff --git a/src/ssl_sock.c b/src/ssl_sock.c index 2bbad178d..e0be9cc78 100644 --- a/src/ssl_sock.c +++ b/src/ssl_sock.c @@ -44,6 +44,9 @@ #include #include #include +#ifdef SSL_CTRL_SET_TLSEXT_STATUS_REQ_CB +#include +#endif #include #include @@ -102,6 +105,350 @@ enum { int sslconns = 0; int totalsslconns = 0; +#ifdef SSL_CTRL_SET_TLSEXT_STATUS_REQ_CB +struct certificate_ocsp { + struct ebmb_node key; + unsigned char key_data[OCSP_MAX_CERTID_ASN1_LENGTH]; + struct chunk response; + +}; + +static struct eb_root cert_ocsp_tree; + +/* This function starts to check if the OCSP response (in DER format) contained + * in chunk 'ocsp_response' is valid (else exits on error). + * If 'cid' is not NULL, it will be compared to the OCSP certificate ID + * contained in the OCSP Response and exits on error if no match. + * If it's a valid OCSP Response: + * If 'ocsp' is not NULL, the chunk is copied in the OCSP response's container + * pointed by 'ocsp'. + * If 'ocsp' is NULL, the function looks up into the OCSP response's + * containers tree (using as index the ASN1 form of the OCSP Certificate ID extracted + * from the response) and exits on error if not found. Finally, If an OCSP response is + * already present in the container, it will be overwritten. + * + * Note: OCSP response containing more than one OCSP Single response is not + * considered valid. + * + * Returns 0 on success, 1 in error case. + */ +static int ssl_sock_load_ocsp_response(struct chunk *ocsp_response, struct certificate_ocsp *ocsp, OCSP_CERTID *cid, char **err) +{ + OCSP_RESPONSE *resp; + OCSP_BASICRESP *bs = NULL; + OCSP_SINGLERESP *sr; + unsigned char *p = (unsigned char *)ocsp_response->str; + int rc , count_sr; + ASN1_GENERALIZEDTIME *revtime, *thisupd, *nextupd; + int reason; + int ret = 1; + + resp = d2i_OCSP_RESPONSE(NULL, (const unsigned char **)&p, ocsp_response->len); + if (!resp) { + memprintf(err, "Unable to parse OCSP response"); + goto out; + } + + rc = OCSP_response_status(resp); + if (rc != OCSP_RESPONSE_STATUS_SUCCESSFUL) { + memprintf(err, "OCSP response status not successful"); + goto out; + } + + bs = OCSP_response_get1_basic(resp); + if (!bs) { + memprintf(err, "Failed to get basic response from OCSP Response"); + goto out; + } + + count_sr = OCSP_resp_count(bs); + if (count_sr > 1) { + memprintf(err, "OCSP response ignored because contains multiple single responses (%d)", count_sr); + goto out; + } + + sr = OCSP_resp_get0(bs, 0); + if (!sr) { + memprintf(err, "Failed to get OCSP single response"); + goto out; + } + + rc = OCSP_single_get0_status(sr, &reason, &revtime, &thisupd, &nextupd); + if (rc != V_OCSP_CERTSTATUS_GOOD) { + memprintf(err, "OCSP single response: certificate status not good"); + goto out; + } + + rc = OCSP_check_validity(thisupd, nextupd, 0, -1); + if (!rc) { + memprintf(err, "OCSP single response: no longer valid."); + goto out; + } + + if (cid) { + if (OCSP_id_cmp(sr->certId, cid)) { + memprintf(err, "OCSP single response: Certificate ID does not match certificate and issuer"); + goto out; + } + } + + if (!ocsp) { + unsigned char key[OCSP_MAX_CERTID_ASN1_LENGTH]; + unsigned char *p; + + rc = i2d_OCSP_CERTID(sr->certId, NULL); + if (!rc) { + memprintf(err, "OCSP single response: Unable to encode Certificate ID"); + goto out; + } + + if (rc > OCSP_MAX_CERTID_ASN1_LENGTH) { + memprintf(err, "OCSP single response: Certificate ID too long"); + goto out; + } + + p = key; + memset(key, 0, OCSP_MAX_CERTID_ASN1_LENGTH); + i2d_OCSP_CERTID(sr->certId, &p); + ocsp = (struct certificate_ocsp *)ebmb_lookup(&cert_ocsp_tree, key, OCSP_MAX_CERTID_ASN1_LENGTH); + if (!ocsp) { + memprintf(err, "OCSP single response: Certificate ID does not match any certificate or issuer"); + goto out; + } + } + + /* According to comments on "chunk_dup", the + previous chunk buffer will be freed */ + if (!chunk_dup(&ocsp->response, ocsp_response)) { + memprintf(err, "OCSP response: Memory allocation error"); + goto out; + } + + ret = 0; +out: + if (bs) + OCSP_BASICRESP_free(bs); + + if (resp) + OCSP_RESPONSE_free(resp); + + return ret; +} +/* + * External function use to update the OCSP response in the OCSP response's + * containers tree. The chunk 'ocsp_response' must contain the OCSP response + * to update in DER format. + * + * Returns 0 on success, 1 in error case. + */ +int ssl_sock_update_ocsp_response(struct chunk *ocsp_response, char **err) +{ + return ssl_sock_load_ocsp_response(ocsp_response, NULL, NULL, err); +} + +/* + * This function load the OCSP Resonse in DER format contained in file at + * path 'ocsp_path' and call 'ssl_sock_load_ocsp_response' + * + * Returns 0 on success, 1 in error case. + */ +static int ssl_sock_load_ocsp_response_from_file(const char *ocsp_path, struct certificate_ocsp *ocsp, OCSP_CERTID *cid, char **err) +{ + int fd = -1; + int r = 0; + int ret = 1; + + fd = open(ocsp_path, O_RDONLY); + if (fd == -1) { + memprintf(err, "Error opening OCSP response file"); + goto end; + } + + trash.len = 0; + while (trash.len < trash.size) { + r = read(fd, trash.str + trash.len, trash.size - trash.len); + if (r < 0) { + if (errno == EINTR) + continue; + + memprintf(err, "Error reading OCSP response from file"); + goto end; + } + else if (r == 0) { + break; + } + trash.len += r; + } + + close(fd); + fd = -1; + + ret = ssl_sock_load_ocsp_response(&trash, ocsp, cid, err); +end: + if (fd != -1) + close(fd); + + return ret; +} + +/* + * Callback used to set OCSP status extension content in server hello. + */ +int ssl_sock_ocsp_stapling_cbk(SSL *ssl, void *arg) +{ + struct certificate_ocsp *ocsp = (struct certificate_ocsp *)arg; + char* ssl_buf; + + if (!ocsp || + !ocsp->response.str || + !ocsp->response.len) + return SSL_TLSEXT_ERR_NOACK; + + ssl_buf = OPENSSL_malloc(ocsp->response.len); + if (!ssl_buf) + return SSL_TLSEXT_ERR_NOACK; + + memcpy(ssl_buf, ocsp->response.str, ocsp->response.len); + SSL_set_tlsext_status_ocsp_resp(ssl, ssl_buf, ocsp->response.len); + + return SSL_TLSEXT_ERR_OK; +} + +/* + * This function enables the handling of OCSP status extension on 'ctx' if a + * file name 'cert_path' suffixed using ".ocsp" is present. + * To enable OCSP status extension, the issuer's certificate is mandatory. + * It should be present in the certificate's extra chain builded from file + * 'cert_path'. If not found, the issuer certificate is loaded from a file + * named 'cert_path' suffixed using '.issuer'. + * + * In addition, ".ocsp" file content is loaded as a DER format of an OCSP + * response. If file is empty or content is not a valid OCSP response, + * OCSP status extension is enabled but OCSP response is ignored (a warning + * is displayed). + * + * Returns 1 if no ".ocsp" file found, 0 if OCSP status extension is + * succesfully enabled, or -1 in other error case. + */ +static int ssl_sock_load_ocsp(SSL_CTX *ctx, const char *cert_path) +{ + + BIO *in = NULL; + X509 *x, *xi = NULL, *issuer = NULL; + STACK_OF(X509) *chain = NULL; + OCSP_CERTID *cid = NULL; + SSL *ssl; + char ocsp_path[MAXPATHLEN+1]; + int i, ret = -1; + struct stat st; + struct certificate_ocsp *ocsp = NULL, *iocsp; + char *warn = NULL; + unsigned char *p; + + snprintf(ocsp_path, MAXPATHLEN+1, "%s.ocsp", cert_path); + + if (stat(ocsp_path, &st)) + return 1; + + ssl = SSL_new(ctx); + if (!ssl) + goto out; + + x = SSL_get_certificate(ssl); + if (!x) + goto out; + + /* Try to lookup for issuer in certificate extra chain */ +#ifdef SSL_CTRL_GET_EXTRA_CHAIN_CERTS + SSL_CTX_get_extra_chain_certs(ctx, &chain); +#else + chain = ctx->extra_certs; +#endif + for (i = 0; i < sk_X509_num(chain); i++) { + issuer = sk_X509_value(chain, i); + if (X509_check_issued(issuer, x) == X509_V_OK) + break; + else + issuer = NULL; + } + + /* If not found try to load issuer from a suffixed file */ + if (!issuer) { + char issuer_path[MAXPATHLEN+1]; + + in = BIO_new(BIO_s_file()); + if (!in) + goto out; + + snprintf(issuer_path, MAXPATHLEN+1, "%s.issuer", cert_path); + if (BIO_read_filename(in, issuer_path) <= 0) + goto out; + + xi = PEM_read_bio_X509_AUX(in, NULL, ctx->default_passwd_callback, ctx->default_passwd_callback_userdata); + if (!xi) + goto out; + + if (X509_check_issued(xi, x) != X509_V_OK) + goto out; + + issuer = xi; + } + + cid = OCSP_cert_to_id(0, x, issuer); + if (!cid) + goto out; + + i = i2d_OCSP_CERTID(cid, NULL); + if (!i || (i > OCSP_MAX_CERTID_ASN1_LENGTH)) + goto out; + + ocsp = calloc(1, sizeof(struct certificate_ocsp)); + if (!ocsp) + goto out; + + p = ocsp->key_data; + i2d_OCSP_CERTID(cid, &p); + + iocsp = (struct certificate_ocsp *)ebmb_insert(&cert_ocsp_tree, &ocsp->key, OCSP_MAX_CERTID_ASN1_LENGTH); + if (iocsp == ocsp) + ocsp = NULL; + + SSL_CTX_set_tlsext_status_cb(ctx, ssl_sock_ocsp_stapling_cbk); + SSL_CTX_set_tlsext_status_arg(ctx, iocsp); + + ret = 0; + + warn = NULL; + if (ssl_sock_load_ocsp_response_from_file(ocsp_path, iocsp, cid, &warn)) { + memprintf(&warn, "Loading '%s': %s. Content will be ignored", ocsp_path, warn ? warn : "failure"); + Warning("%s.\n", warn); + } + +out: + if (ssl) + SSL_free(ssl); + + if (in) + BIO_free(in); + + if (xi) + X509_free(xi); + + if (cid) + OCSP_CERTID_free(cid); + + if (ocsp) + free(ocsp); + + if (warn) + free(warn); + + + return ret; +} + +#endif + void ssl_sock_infocbk(const SSL *ssl, int where, int ret) { struct connection *conn = (struct connection *)SSL_get_app_data(ssl); @@ -838,6 +1185,16 @@ static int ssl_sock_load_cert_file(const char *path, struct bind_conf *bind_conf } #endif +#ifdef SSL_CTRL_SET_TLSEXT_STATUS_REQ_CB + ret = ssl_sock_load_ocsp(ctx, path); + if (ret < 0) { + if (err) + memprintf(err, "%s '%s.ocsp' is present and activates OCSP but it is impossible to compute the OCSP certificate ID (maybe the issuer could not be found)'.\n", + *err ? *err : "", path); + return 1; + } +#endif + #ifndef SSL_CTRL_SET_TLSEXT_HOSTNAME if (bind_conf->default_ctx) { memprintf(err, "%sthis version of openssl cannot load multiple SSL certificates.\n",