MEDIUM: ssl: basic OCSP stapling support.

The support is all based on static responses. This doesn't add any
request / response logic to HAProxy, but allows a way to update
information through the socket interface.

Currently certificates specified using "crt" or "crt-list" on "bind" lines
are loaded as PEM files.
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.

It is possible to update an OCSP Response from the unix socket using:

  set ssl ocsp-response <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 <response> 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

This feature is automatically enabled on openssl 0.9.8h and above.

This work was performed jointly by Dirkjan Bussink of GitHub and
Emeric Brun of HAProxy Technologies.
diff --git a/doc/configuration.txt b/doc/configuration.txt
index 370fd53..6d215eb 100644
--- a/doc/configuration.txt
+++ b/doc/configuration.txt
@@ -8264,7 +8264,8 @@
   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 @@
   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 <errors>
   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 @@
   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 <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 <response> 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 <table> key <key> [data.<data_type> <value>]*
   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 8d5d62a..0d18281 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 2d1dadc..0902fde 100644
--- a/include/proto/ssl_sock.h
+++ b/include/proto/ssl_sock.h
@@ -54,6 +54,9 @@
 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 bc0bea7..36b4684 100644
--- a/src/dumpstats.c
+++ b/src/dumpstats.c
@@ -35,6 +35,7 @@
 #include <common/time.h>
 #include <common/uri_auth.h>
 #include <common/version.h>
+#include <common/base64.h>
 
 #include <types/global.h>
 
@@ -195,6 +196,7 @@
 	"  add map        : add map entry\n"
 	"  del map        : delete map entry\n"
 	"  clear map <id> : clear the content of this map\n"
+	"  set ssl <stmt> : set statement for ssl\n"
 	"";
 
 static const char stats_permission_denied_msg[] =
@@ -1789,6 +1791,50 @@
 			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 2bbad17..e0be9cc 100644
--- a/src/ssl_sock.c
+++ b/src/ssl_sock.c
@@ -44,6 +44,9 @@
 #include <openssl/x509.h>
 #include <openssl/err.h>
 #include <openssl/rand.h>
+#ifdef SSL_CTRL_SET_TLSEXT_STATUS_REQ_CB
+#include <openssl/ocsp.h>
+#endif
 
 #include <common/buffer.h>
 #include <common/compat.h>
@@ -102,6 +105,350 @@
 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 @@
 	}
 #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",