diff options
Diffstat (limited to 'src/ssl_ocsp.c')
-rw-r--r-- | src/ssl_ocsp.c | 1986 |
1 files changed, 1986 insertions, 0 deletions
diff --git a/src/ssl_ocsp.c b/src/ssl_ocsp.c new file mode 100644 index 0000000..1adddc4 --- /dev/null +++ b/src/ssl_ocsp.c @@ -0,0 +1,1986 @@ + +/* + * SSL/TLS OCSP-related functions + * + * Copyright (C) 2022 HAProxy Technologies, Remi Tricot-Le Breton <rlebreton@haproxy.com> + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version + * 2 of the License, or (at your option) any later version. + * + * Acknowledgement: + * We'd like to specially thank the Stud project authors for a very clean + * and well documented code which helped us understand how the OpenSSL API + * ought to be used in non-blocking mode. This is one difficult part which + * is not easy to get from the OpenSSL doc, and reading the Stud code made + * it much more obvious than the examples in the OpenSSL package. Keep up + * the good works, guys ! + * + * Stud is an extremely efficient and scalable SSL/TLS proxy which combines + * particularly well with haproxy. For more info about this project, visit : + * https://github.com/bumptech/stud + * + */ + +/* Note: do NOT include openssl/xxx.h here, do it in openssl-compat.h */ +#define _GNU_SOURCE +#include <ctype.h> +#include <dirent.h> +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include <sys/socket.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <netdb.h> +#include <netinet/tcp.h> + +#include <import/ebpttree.h> +#include <import/ebsttree.h> +#include <import/lru.h> + +#include <haproxy/api.h> +#include <haproxy/applet.h> +#include <haproxy/arg.h> +#include <haproxy/base64.h> +#include <haproxy/channel.h> +#include <haproxy/chunk.h> +#include <haproxy/cli.h> +#include <haproxy/connection.h> +#include <haproxy/dynbuf.h> +#include <haproxy/errors.h> +#include <haproxy/fd.h> +#include <haproxy/freq_ctr.h> +#include <haproxy/frontend.h> +#include <haproxy/global.h> +#include <haproxy/http_rules.h> +#include <haproxy/log.h> +#include <haproxy/openssl-compat.h> +#include <haproxy/pattern-t.h> +#include <haproxy/proto_tcp.h> +#include <haproxy/proxy.h> +#include <haproxy/sample.h> +#include <haproxy/sc_strm.h> +#include <haproxy/quic_conn.h> +#include <haproxy/quic_tp.h> +#include <haproxy/server.h> +#include <haproxy/shctx.h> +#include <haproxy/ssl_ckch.h> +#include <haproxy/ssl_crtlist.h> +#include <haproxy/ssl_sock.h> +#include <haproxy/ssl_utils.h> +#include <haproxy/stats.h> +#include <haproxy/stconn.h> +#include <haproxy/stream-t.h> +#include <haproxy/task.h> +#include <haproxy/ticks.h> +#include <haproxy/time.h> +#include <haproxy/tools.h> +#include <haproxy/vars.h> +#include <haproxy/xxhash.h> +#include <haproxy/istbuf.h> +#include <haproxy/ssl_ocsp-t.h> +#include <haproxy/http_client.h> + + +/* ***** READ THIS before adding code here! ***** + * + * Due to API incompatibilities between multiple OpenSSL versions and their + * derivatives, it's often tempting to add macros to (re-)define certain + * symbols. Please do not do this here, and do it in common/openssl-compat.h + * exclusively so that the whole code consistently uses the same macros. + * + * Whenever possible if a macro is missing in certain versions, it's better + * to conditionally define it in openssl-compat.h than using lots of ifdefs. + */ + +#ifndef OPENSSL_NO_OCSP +int ocsp_ex_index = -1; + +int ssl_sock_get_ocsp_arg_kt_index(int evp_keytype) +{ + switch (evp_keytype) { + case EVP_PKEY_RSA: + return 2; + case EVP_PKEY_DSA: + return 0; + case EVP_PKEY_EC: + return 1; + } + + return -1; +} + +/* + * 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 ocsp_cbk_arg *ocsp_arg; + char *ssl_buf; + SSL_CTX *ctx; + EVP_PKEY *ssl_pkey; + int key_type; + int index; + + ctx = SSL_get_SSL_CTX(ssl); + if (!ctx) + return SSL_TLSEXT_ERR_NOACK; + + ocsp_arg = SSL_CTX_get_ex_data(ctx, ocsp_ex_index); + if (!ocsp_arg) + return SSL_TLSEXT_ERR_NOACK; + + ssl_pkey = SSL_get_privatekey(ssl); + if (!ssl_pkey) + return SSL_TLSEXT_ERR_NOACK; + + key_type = EVP_PKEY_base_id(ssl_pkey); + + if (ocsp_arg->is_single && ocsp_arg->single_kt == key_type) + ocsp = ocsp_arg->s_ocsp; + else { + /* For multiple certs per context, we have to find the correct OCSP response based on + * the certificate type + */ + index = ssl_sock_get_ocsp_arg_kt_index(key_type); + + if (index < 0) + return SSL_TLSEXT_ERR_NOACK; + + ocsp = ocsp_arg->m_ocsp[index]; + + } + + if (!ocsp || + !ocsp->response.area || + !ocsp->response.data || + (ocsp->expire < date.tv_sec)) + return SSL_TLSEXT_ERR_NOACK; + + ssl_buf = OPENSSL_malloc(ocsp->response.data); + if (!ssl_buf) + return SSL_TLSEXT_ERR_NOACK; + + memcpy(ssl_buf, ocsp->response.area, ocsp->response.data); + SSL_set_tlsext_status_ocsp_resp(ssl, (unsigned char*)ssl_buf, ocsp->response.data); + + return SSL_TLSEXT_ERR_OK; +} + +#endif /* !defined(OPENSSL_NO_OCSP) */ + + +#if (defined SSL_CTRL_SET_TLSEXT_STATUS_REQ_CB && !defined OPENSSL_NO_OCSP) + +struct eb_root cert_ocsp_tree = EB_ROOT_UNIQUE; + +__decl_thread(HA_SPINLOCK_T ocsp_tree_lock); + +struct eb_root ocsp_update_tree = EB_ROOT; /* updatable ocsp responses sorted by next_update in absolute time */ + +/* + * Convert an OCSP_CERTID structure into a char buffer that can be used as a key + * in the OCSP response tree. It takes an <ocsp_cid> as parameter and builds a + * key of length <key_length> into the <certid> buffer. The key length cannot + * exceed OCSP_MAX_CERTID_ASN1_LENGTH bytes. + * Returns a negative value in case of error. + */ +int ssl_ocsp_build_response_key(OCSP_CERTID *ocsp_cid, unsigned char certid[OCSP_MAX_CERTID_ASN1_LENGTH], unsigned int *key_length) +{ + unsigned char *p = NULL; + int i; + + if (!key_length) + return -1; + + *key_length = 0; + + if (!ocsp_cid) + return 0; + + i = i2d_OCSP_CERTID(ocsp_cid, NULL); + if (!i || (i > OCSP_MAX_CERTID_ASN1_LENGTH)) + return 0; + + p = certid; + *key_length = i2d_OCSP_CERTID(ocsp_cid, &p); + +end: + return *key_length > 0; +} + +/* 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. + */ +int ssl_sock_load_ocsp_response(struct buffer *ocsp_response, + struct certificate_ocsp *ocsp, + OCSP_CERTID *cid, char **err) +{ + OCSP_RESPONSE *resp; + OCSP_BASICRESP *bs = NULL; + OCSP_SINGLERESP *sr; + OCSP_CERTID *id; + unsigned char *p = (unsigned char *) ocsp_response->area; + int rc , count_sr; + ASN1_GENERALIZEDTIME *revtime, *thisupd, *nextupd = NULL; + int reason; + int ret = 1; +#ifdef HAVE_ASN1_TIME_TO_TM + struct tm nextupd_tm = {0}; +#endif + + resp = d2i_OCSP_RESPONSE(NULL, (const unsigned char **)&p, + ocsp_response->data); + 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; + } + + id = (OCSP_CERTID*)OCSP_SINGLERESP_get0_id(sr); + + rc = OCSP_single_get0_status(sr, &reason, &revtime, &thisupd, &nextupd); + if (rc != V_OCSP_CERTSTATUS_GOOD && rc != V_OCSP_CERTSTATUS_REVOKED) { + memprintf(err, "OCSP single response: certificate status is unknown"); + goto out; + } + + if (!nextupd) { + memprintf(err, "OCSP single response: missing nextupdate"); + goto out; + } + + rc = OCSP_check_validity(thisupd, nextupd, OCSP_MAX_RESPONSE_TIME_SKEW, -1); + if (!rc) { + memprintf(err, "OCSP single response: no longer valid."); + goto out; + } + + if (cid) { + if (OCSP_id_cmp(id, 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(id, 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(id, &p); + HA_SPIN_LOCK(OCSP_LOCK, &ocsp_tree_lock); + ocsp = (struct certificate_ocsp *)ebmb_lookup(&cert_ocsp_tree, key, OCSP_MAX_CERTID_ASN1_LENGTH); + if (!ocsp) { + HA_SPIN_UNLOCK(OCSP_LOCK, &ocsp_tree_lock); + memprintf(err, "OCSP single response: Certificate ID does not match any certificate or issuer"); + goto out; + } + HA_SPIN_UNLOCK(OCSP_LOCK, &ocsp_tree_lock); + } + + /* 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; + } + +#ifdef HAVE_ASN1_TIME_TO_TM + if (ASN1_TIME_to_tm(nextupd, &nextupd_tm) == 0) { + memprintf(err, "OCSP single response: Invalid \"Next Update\" time"); + goto out; + } + ocsp->expire = my_timegm(&nextupd_tm) - OCSP_MAX_RESPONSE_TIME_SKEW; +#else + ocsp->expire = asn1_generalizedtime_to_epoch(nextupd) - OCSP_MAX_RESPONSE_TIME_SKEW; + if (ocsp->expire < 0) { + memprintf(err, "OCSP single response: Invalid \"Next Update\" time"); + goto out; + } +#endif + + ret = 0; +out: + ERR_clear_error(); + + 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 buffer *ocsp_response, char **err) +{ + return ssl_sock_load_ocsp_response(ocsp_response, NULL, NULL, err); +} + + + +#if !defined OPENSSL_IS_BORINGSSL +/* + * Decrease the refcount of the struct ocsp_response and frees it if it's not + * used anymore. Also removes it from the tree if free'd. + */ +void ssl_sock_free_ocsp(struct certificate_ocsp *ocsp) +{ + if (!ocsp) + return; + + HA_SPIN_LOCK(OCSP_LOCK, &ocsp_tree_lock); + ocsp->refcount_store--; + if (ocsp->refcount_store <= 0) { + BUG_ON(ocsp->refcount_instance > 0); + ebmb_delete(&ocsp->key); + eb64_delete(&ocsp->next_update); + X509_free(ocsp->issuer); + ocsp->issuer = NULL; + sk_X509_pop_free(ocsp->chain, X509_free); + ocsp->chain = NULL; + chunk_destroy(&ocsp->response); + if (ocsp->uri) { + ha_free(&ocsp->uri->area); + ha_free(&ocsp->uri); + } + + free(ocsp); + } + HA_SPIN_UNLOCK(OCSP_LOCK, &ocsp_tree_lock); +} + +void ssl_sock_free_ocsp_instance(struct certificate_ocsp *ocsp) +{ + if (!ocsp) + return; + + HA_SPIN_LOCK(OCSP_LOCK, &ocsp_tree_lock); + ocsp->refcount_instance--; + if (ocsp->refcount_instance <= 0) { + eb64_delete(&ocsp->next_update); + } + HA_SPIN_UNLOCK(OCSP_LOCK, &ocsp_tree_lock); +} + + +/* + * This function dumps the details of an OCSP_CERTID. It is based on + * ocsp_certid_print in OpenSSL. + */ +static inline int ocsp_certid_print(BIO *bp, OCSP_CERTID *certid, int indent) +{ + ASN1_OCTET_STRING *piNameHash = NULL; + ASN1_OCTET_STRING *piKeyHash = NULL; + ASN1_INTEGER *pSerial = NULL; + + if (OCSP_id_get0_info(&piNameHash, NULL, &piKeyHash, &pSerial, certid)) { + + BIO_printf(bp, "%*sCertificate ID:\n", indent, ""); + indent += 2; + BIO_printf(bp, "%*sIssuer Name Hash: ", indent, ""); +#ifndef USE_OPENSSL_WOLFSSL + i2a_ASN1_STRING(bp, piNameHash, 0); +#else + wolfSSL_ASN1_STRING_print(bp, piNameHash); +#endif + BIO_printf(bp, "\n%*sIssuer Key Hash: ", indent, ""); +#ifndef USE_OPENSSL_WOLFSSL + i2a_ASN1_STRING(bp, piKeyHash, 0); +#else + wolfSSL_ASN1_STRING_print(bp, piNameHash); +#endif + BIO_printf(bp, "\n%*sSerial Number: ", indent, ""); + i2a_ASN1_INTEGER(bp, pSerial); + } + return 1; +} + + +enum { + SHOW_OCSPRESP_FMT_DFLT, + SHOW_OCSPRESP_FMT_TEXT, + SHOW_OCSPRESP_FMT_B64 +}; + +struct show_ocspresp_cli_ctx { + struct certificate_ocsp *ocsp; + int format; +}; + +/* + * Dump the details about an OCSP response in DER format stored in + * <ocsp_response> into buffer <out>. + * Returns 0 in case of success. + */ +int ssl_ocsp_response_print(struct buffer *ocsp_response, struct buffer *out) +{ + BIO *bio = NULL; + int write = -1; + OCSP_RESPONSE *resp; + const unsigned char *p; + int retval = -1; + + if (!ocsp_response) + return -1; + + if ((bio = BIO_new(BIO_s_mem())) == NULL) + return -1; + + p = (const unsigned char*)ocsp_response->area; + + resp = d2i_OCSP_RESPONSE(NULL, &p, ocsp_response->data); + if (!resp) { + chunk_appendf(out, "Unable to parse OCSP response"); + goto end; + } + +#ifndef USE_OPENSSL_WOLFSSL + if (OCSP_RESPONSE_print(bio, resp, 0) != 0) { +#else + if (wolfSSL_d2i_OCSP_RESPONSE_bio(bio, &resp) != 0) { +#endif + struct buffer *trash = get_trash_chunk(); + struct ist ist_block = IST_NULL; + struct ist ist_double_lf = IST_NULL; + static struct ist double_lf = IST("\n\n"); + + write = BIO_read(bio, trash->area, trash->size - 1); + if (write <= 0) + goto end; + trash->data = write; + + /* Look for empty lines in the 'trash' buffer and add a space to + * the beginning to avoid having empty lines in the output + * (without changing the appearance of the information + * displayed). + */ + ist_block = ist2(b_orig(trash), b_data(trash)); + + ist_double_lf = istist(ist_block, double_lf); + + while (istlen(ist_double_lf)) { + /* istptr(ist_double_lf) points to the first \n of a + * \n\n pattern. + */ + uint empty_line_offset = istptr(ist_double_lf) + 1 - istptr(ist_block); + + /* Write up to the first '\n' of the "\n\n" pattern into + * the output buffer. + */ + b_putblk(out, istptr(ist_block), empty_line_offset); + /* Add an extra space. */ + b_putchr(out, ' '); + + /* Keep looking for empty lines in the rest of the data. */ + ist_block = istadv(ist_block, empty_line_offset); + + ist_double_lf = istist(ist_block, double_lf); + } + + retval = (b_istput(out, ist_block) <= 0); + } + +end: + if (bio) + BIO_free(bio); + + OCSP_RESPONSE_free(resp); + + return retval; +} + +/* + * Dump the contents of an OCSP response in DER format stored in + * <ocsp_response> into buffer <out> after converting it to base64. + * Returns 0 in case of success. + */ +static int ssl_ocsp_response_print_base64(struct buffer *ocsp_response, struct buffer *out) +{ + int b64len = 0; + + b64len = a2base64(b_orig(ocsp_response), b_data(ocsp_response), + b_orig(out), b_size(out)); + + if (b64len < 0) + return 1; + + out->data = b64len; + + /* Add empty line */ + chunk_appendf(ocsp_response, "\n"); + + return 0; +} + +/* + * Dump the details of the OCSP response of ID <ocsp_certid> into buffer <out>. + * Returns 0 in case of success. + */ +int ssl_get_ocspresponse_detail(unsigned char *ocsp_certid, struct buffer *out) +{ + struct certificate_ocsp *ocsp; + int ret = 0; + + HA_SPIN_LOCK(OCSP_LOCK, &ocsp_tree_lock); + ocsp = (struct certificate_ocsp *)ebmb_lookup(&cert_ocsp_tree, ocsp_certid, OCSP_MAX_CERTID_ASN1_LENGTH); + if (!ocsp) { + HA_SPIN_UNLOCK(OCSP_LOCK, &ocsp_tree_lock); + return -1; + } + + ret = ssl_ocsp_response_print(&ocsp->response, out); + + HA_SPIN_UNLOCK(OCSP_LOCK, &ocsp_tree_lock); + + return ret; +} + + +/* IO handler of details "show ssl ocsp-response <id>". + * The current entry is taken from appctx->svcctx. + */ +static int cli_io_handler_show_ocspresponse_detail(struct appctx *appctx) +{ + struct buffer *trash = get_trash_chunk(); + struct show_ocspresp_cli_ctx *ctx = appctx->svcctx; + struct certificate_ocsp *ocsp = ctx->ocsp; + int retval = 0; + + switch (ctx->format) { + case SHOW_OCSPRESP_FMT_DFLT: + case SHOW_OCSPRESP_FMT_TEXT: + retval = ssl_ocsp_response_print(&ocsp->response, trash); + break; + case SHOW_OCSPRESP_FMT_B64: + retval = ssl_ocsp_response_print_base64(&ocsp->response, trash); + break; + } + + if (retval) + return 1; + + if (applet_putchk(appctx, trash) == -1) + goto yield; + + appctx->svcctx = NULL; + return 1; + +yield: + return 0; +} + +void ssl_sock_ocsp_free_func(void *parent, void *ptr, CRYPTO_EX_DATA *ad, int idx, long argl, void *argp) +{ + struct ocsp_cbk_arg *ocsp_arg; + + if (ptr) { + ocsp_arg = ptr; + + if (ocsp_arg->is_single) { + ssl_sock_free_ocsp_instance(ocsp_arg->s_ocsp); + ocsp_arg->s_ocsp = NULL; + } else { + int i; + + for (i = 0; i < SSL_SOCK_NUM_KEYTYPES; i++) { + ssl_sock_free_ocsp_instance(ocsp_arg->m_ocsp[i]); + ocsp_arg->m_ocsp[i] = NULL; + } + } + free(ocsp_arg); + } +} + +/* + * Extract the first OCSP URI (if any) contained in <cert> and write it into + * <out>. + * Returns 0 in case of success, 1 otherwise. + */ +int ssl_ocsp_get_uri_from_cert(X509 *cert, struct buffer *out, char **err) +{ + STACK_OF(OPENSSL_STRING) *ocsp_uri_stk = NULL; + int ret = 1; + + if (!cert || !out) + goto end; + + ocsp_uri_stk = X509_get1_ocsp(cert); + if (ocsp_uri_stk == NULL) { + memprintf(err, "%sNo OCSP URL stack!\n", *err ? *err : ""); + goto end; + } + + if (!chunk_strcpy(out, sk_OPENSSL_STRING_value(ocsp_uri_stk, 0))) { + memprintf(err, "%sOCSP URI too long!\n", *err ? *err : ""); + goto end; + } + if (b_data(out) == 0) { + memprintf(err, "%sNo OCSP URL!\n", *err ? *err : ""); + goto end; + } + + ret = 0; + +end: + X509_email_free(ocsp_uri_stk); + return ret; +} + +/* + * Create the url and request body that make a proper OCSP request for the + * <certid>. The <req_url> parameter should already hold the OCSP URI that was + * extracted from the corresponding certificate. Depending on the size of the + * certid we will either append data to the <req_url> to create a proper URL + * that will be sent with a GET command, or the <req_body> will be constructed + * in case of a POST. + * Returns 0 in case of success. + */ +int ssl_ocsp_create_request_details(const OCSP_CERTID *certid, struct buffer *req_url, + struct buffer *req_body, char **err) +{ + int errcode = -1; + OCSP_REQUEST *ocsp; + struct buffer *bin_request = get_trash_chunk(); + unsigned char *outbuf = (unsigned char*)b_orig(bin_request); + + ocsp = OCSP_REQUEST_new(); + if (ocsp == NULL) { + memprintf(err, "%sCan't create OCSP_REQUEST\n", *err ? *err : ""); + goto end; + } + + if (OCSP_request_add0_id(ocsp, (OCSP_CERTID*)certid) == NULL) { + memprintf(err, "%sOCSP_request_add0_id() error\n", *err ? *err : ""); + goto end; + } + + bin_request->data = i2d_OCSP_REQUEST(ocsp, &outbuf); + if (b_data(bin_request) <= 0) { + memprintf(err, "%si2d_OCSP_REQUEST() error\n", *err ? *err : ""); + goto end; + } + + /* HTTP based OCSP requests can use either the GET or the POST method to + * submit their requests. To enable HTTP caching, small requests (that + * after encoding are less than 255 bytes), MAY be submitted using GET. + * If HTTP caching is not important, or the request is greater than 255 + * bytes, the request SHOULD be submitted using POST. + */ + if (b_data(bin_request) + b_data(req_url) < 0xff) { + struct buffer *b64buf = get_trash_chunk(); + char *ret = NULL; + int base64_ret = 0; + + chunk_strcat(req_url, "/"); + + base64_ret = a2base64(b_orig(bin_request), b_data(bin_request), + b_orig(b64buf), b_size(b64buf)); + + if (base64_ret < 0) { + memprintf(err, "%sa2base64() error\n", *err ? *err : ""); + goto end; + } + + b64buf->data = base64_ret; + + ret = encode_chunk((char*)b_stop(req_url), b_orig(req_url) + b_size(req_url), '%', + query_encode_map, b64buf); + if (ret && *ret == '\0') { + req_url->data = ret - b_orig(req_url); + errcode = 0; + } + } + else { + chunk_cpy(req_body, bin_request); + errcode = 0; + } + + +end: + OCSP_REQUEST_free(ocsp); + + return errcode; +} + +/* + * Parse an OCSP_RESPONSE contained in <respbuf> and check its validity in + * regard to the contents of <ckch> or the <issuer> certificate. + * Certificate_ocsp structure does not keep a reference to the corresponding + * ckch_store so outside of a CLI context (see "send ssl ocsp-response" + * command), we only have an easy access to the issuer's certificate whose + * reference is held in the structure. + * Return 0 in case of success, 1 otherwise. + */ +int ssl_ocsp_check_response(STACK_OF(X509) *chain, X509 *issuer, + struct buffer *respbuf, char **err) +{ + int ret = 1; + int n; + OCSP_RESPONSE *response = NULL; + OCSP_BASICRESP *basic = NULL; + X509_STORE *store = NULL; + const unsigned char *start = (const unsigned char*)b_orig(respbuf); + + if (!chain && !issuer) { + memprintf(err, "check_ocsp_response needs a certificate validation chain or an issuer certificate"); + goto end; + } + + response = d2i_OCSP_RESPONSE(NULL, &start, b_data(respbuf)); + if (!response) { + memprintf(err, "d2i_OCSP_RESPONSE() failed"); + goto end; + } + + n = OCSP_response_status(response); + + if (n != OCSP_RESPONSE_STATUS_SUCCESSFUL) { + memprintf(err, "OCSP response not successful (%d: %s)", + n, OCSP_response_status_str(n)); + goto end; + } + + basic = OCSP_response_get1_basic(response); + if (basic == NULL) { + memprintf(err, "OCSP_response_get1_basic() failed"); + goto end; + } + + /* Create a temporary store in which we add the certificate's chain + * certificates. We assume that all those certificates can be trusted + * because they were provided by the user. + * The only ssl item that needs to be verified here is the OCSP + * response. + */ + store = X509_STORE_new(); + if (!store) { + memprintf(err, "X509_STORE_new() failed"); + goto end; + } + + if (chain) { + int i = 0; + for (i = 0; i < sk_X509_num(chain); i++) { + X509 *cert = sk_X509_value(chain, i); + X509_STORE_add_cert(store, cert); + } + } + + if (issuer) + X509_STORE_add_cert(store, issuer); + + if (OCSP_basic_verify(basic, chain, store, OCSP_TRUSTOTHER) != 1) { + memprintf(err, "OCSP_basic_verify() failed"); + goto end; + } + + ret = 0; + +end: + X509_STORE_free(store); + OCSP_RESPONSE_free(response); + OCSP_BASICRESP_free(basic); + return ret; +} + + +/* + * OCSP-UPDATE RELATED FUNCTIONS AND STRUCTURES + */ + +struct task *ocsp_update_task __read_mostly = NULL; +static struct proxy *httpclient_ocsp_update_px; + +static struct ssl_ocsp_task_ctx { + struct certificate_ocsp *cur_ocsp; + struct httpclient *hc; + struct appctx *appctx; + int flags; + int update_status; +} ssl_ocsp_task_ctx; + +const struct http_hdr ocsp_request_hdrs[] = { + { IST("Content-Type"), IST("application/ocsp-request") }, + { IST_NULL, IST_NULL } +}; + +enum { + OCSP_UPDT_UNKNOWN = 0, + OCSP_UPDT_OK = 1, + OCSP_UPDT_ERR_HTTP_STATUS = 2, + OCSP_UPDT_ERR_HTTP_HDR = 3, + OCSP_UPDT_ERR_CHECK = 4, + OCSP_UPDT_ERR_INSERT = 5, + OCSP_UPDT_ERR_LAST /* Must be last */ +}; + +const struct ist ocsp_update_errors[] = { + [OCSP_UPDT_UNKNOWN] = IST("Unknown"), + [OCSP_UPDT_OK] = IST("Update successful"), + [OCSP_UPDT_ERR_HTTP_STATUS] = IST("HTTP error"), + [OCSP_UPDT_ERR_HTTP_HDR] = IST("Missing \"ocsp-response\" header"), + [OCSP_UPDT_ERR_CHECK] = IST("OCSP response check failure"), + [OCSP_UPDT_ERR_INSERT] = IST("Error during insertion") +}; + +static struct task *ssl_ocsp_update_responses(struct task *task, void *context, unsigned int state); + +/* + * Create the main OCSP update task that will iterate over the OCSP responses + * stored in ocsp_update_tree and send an OCSP request via the http_client + * applet to the corresponding OCSP responder. The task will then be in charge + * of processing the response, verifying it and resinserting it in the actual + * ocsp response tree if the response is valid. + * Returns 0 in case of success. + */ +int ssl_create_ocsp_update_task(char **err) +{ + if (ocsp_update_task) + return 0; /* Already created */ + + ocsp_update_task = task_new_anywhere(); + if (!ocsp_update_task) { + memprintf(err, "parsing : failed to allocate global ocsp update task."); + return -1; + } + + ocsp_update_task->process = ssl_ocsp_update_responses; + ocsp_update_task->context = NULL; + + return 0; +} + +static int ssl_ocsp_task_schedule() +{ + if (ocsp_update_task) + task_schedule(ocsp_update_task, now_ms); + + return 0; +} +REGISTER_POST_CHECK(ssl_ocsp_task_schedule); + +void ssl_sock_free_ocsp(struct certificate_ocsp *ocsp); + +void ssl_destroy_ocsp_update_task(void) +{ + struct eb64_node *node, *next; + if (!ocsp_update_task) + return; + + HA_SPIN_LOCK(OCSP_LOCK, &ocsp_tree_lock); + + node = eb64_first(&ocsp_update_tree); + while (node) { + next = eb64_next(node); + eb64_delete(node); + node = next; + } + + HA_SPIN_UNLOCK(OCSP_LOCK, &ocsp_tree_lock); + + task_destroy(ocsp_update_task); + ocsp_update_task = NULL; + + ssl_sock_free_ocsp(ssl_ocsp_task_ctx.cur_ocsp); + ssl_ocsp_task_ctx.cur_ocsp = NULL; + + if (ssl_ocsp_task_ctx.hc) { + httpclient_stop_and_destroy(ssl_ocsp_task_ctx.hc); + ssl_ocsp_task_ctx.hc = NULL; + } +} + +static inline void ssl_ocsp_set_next_update(struct certificate_ocsp *ocsp) +{ + int update_margin = (ocsp->expire >= SSL_OCSP_UPDATE_MARGIN) ? SSL_OCSP_UPDATE_MARGIN : 0; + + ocsp->next_update.key = MIN(date.tv_sec + global_ssl.ocsp_update.delay_max, + ocsp->expire - update_margin); + + /* An already existing valid OCSP response that expires within less than + * SSL_OCSP_UPDATE_DELAY_MIN or has no 'Next Update' field should not be + * updated more than once every 5 minutes in order to avoid continuous + * update of the same response. */ + if (b_data(&ocsp->response)) + ocsp->next_update.key = MAX(ocsp->next_update.key, + date.tv_sec + global_ssl.ocsp_update.delay_min); +} + +/* + * Insert a certificate_ocsp structure into the ocsp_update_tree tree, in which + * entries are sorted by absolute date of the next update. The next_update key + * will be the smallest out of the actual expire value of the response and + * now+1H. This arbitrary 1H value ensures that ocsp responses are updated + * periodically even when they have a long expire time, while not overloading + * the system too much (in theory). Likewise, a minimum 5 minutes interval is + * defined in order to avoid updating too often responses that have a really + * short expire time or even no 'Next Update' at all. + */ +int ssl_ocsp_update_insert(struct certificate_ocsp *ocsp) +{ + /* Set next_update based on current time and the various OCSP + * minimum/maximum update times. + */ + ssl_ocsp_set_next_update(ocsp); + + ocsp->fail_count = 0; + + HA_SPIN_LOCK(OCSP_LOCK, &ocsp_tree_lock); + ocsp->updating = 0; + /* An entry with update_once set to 1 was only supposed to be updated + * once, it does not need to be reinserted into the update tree. + */ + if (!ocsp->update_once) + eb64_insert(&ocsp_update_tree, &ocsp->next_update); + HA_SPIN_UNLOCK(OCSP_LOCK, &ocsp_tree_lock); + + return 0; +} + +/* + * Reinsert an entry in the update tree. The entry's next update time can not + * occur before now+SSL_OCSP_HTTP_ERR_REPLAY. + * This is supposed to be used in case of http error (ocsp responder unreachable + * for instance). This ensures that the entry does not get reinserted at the + * beginning of the tree every time. + */ +int ssl_ocsp_update_insert_after_error(struct certificate_ocsp *ocsp) +{ + int replay_delay = 0; + + /* + * Set next_update based on current time and the various OCSP + * minimum/maximum update times. + */ + ssl_ocsp_set_next_update(ocsp); + + ++ocsp->fail_count; + + /* + * The replay delay will be increased for every consecutive update + * failure, up to the SSL_OCSP_UPDATE_DELAY_MAX delay. It will ensure + * that the replay delay will be one minute for the first failure and + * will be multiplied by 2 for every subsequent failures, while still + * being at most 1 hour (with the current default values). + */ + replay_delay = MIN(SSL_OCSP_HTTP_ERR_REPLAY * (1 << ocsp->fail_count), + global_ssl.ocsp_update.delay_max); + + if (ocsp->next_update.key < date.tv_sec + replay_delay) + ocsp->next_update.key = date.tv_sec + replay_delay; + + HA_SPIN_LOCK(OCSP_LOCK, &ocsp_tree_lock); + ocsp->updating = 0; + /* An entry with update_once set to 1 was only supposed to be updated + * once, it does not need to be reinserted into the update tree. + */ + if (!ocsp->update_once) + eb64_insert(&ocsp_update_tree, &ocsp->next_update); + HA_SPIN_UNLOCK(OCSP_LOCK, &ocsp_tree_lock); + + return 0; +} + +void ocsp_update_response_stline_cb(struct httpclient *hc) +{ + struct task *task = hc->caller; + + if (!task) + return; + + ssl_ocsp_task_ctx.flags |= HC_F_RES_STLINE; + task_wakeup(task, TASK_WOKEN_MSG); +} + +void ocsp_update_response_headers_cb(struct httpclient *hc) +{ + struct task *task = hc->caller; + + if (!task) + return; + + ssl_ocsp_task_ctx.flags |= HC_F_RES_HDR; + task_wakeup(task, TASK_WOKEN_MSG); +} + +void ocsp_update_response_body_cb(struct httpclient *hc) +{ + struct task *task = hc->caller; + + if (!task) + return; + + ssl_ocsp_task_ctx.flags |= HC_F_RES_BODY; + task_wakeup(task, TASK_WOKEN_MSG); +} + +void ocsp_update_response_end_cb(struct httpclient *hc) +{ + struct task *task = hc->caller; + + if (!task) + return; + + ssl_ocsp_task_ctx.flags |= HC_F_RES_END; + task_wakeup(task, TASK_WOKEN_MSG); +} + + +/* + * Send a log line that will use the dedicated proxy's error_logformat string. + * It uses the sess_log function instead of app_log for instance in order to + * benefit from the "generic" items that can be added to a log format line such + * as the date and frontend name that can be found at the beginning of the + * ocspupdate_log_format line. + */ +static void ssl_ocsp_send_log() +{ + if (!ssl_ocsp_task_ctx.appctx) + return; + + sess_log(ssl_ocsp_task_ctx.appctx->sess); +} + +/* + * This is the main function of the ocsp auto update mechanism. It has two + * distinct parts and the branching to one or the other is completely based on + * the fact that the cur_ocsp pointer of the ssl_ocsp_task_ctx member is set. + * + * If the pointer is not set, we need to look at the first item of the update + * tree and see if it needs to be updated. If it does not we simply wait until + * the time is right and let the task asleep. If it does need to be updated, we + * simply build and send the corresponding ocsp request thanks to the + * http_client. The task is then sent to sleep with an expire time set to + * infinity. The http_client will wake it back up once the response is received + * (or a timeout occurs). Just note that during this whole process the + * cetificate_ocsp object corresponding to the entry being updated is taken out + * of the update tree and only stored in the ssl_ocsp_task_ctx context. + * + * Once the task is waken up by the http_client, it branches on the response + * processing part of the function which basically checks that the response is + * valid and inserts it into the ocsp_response tree. The task then goes back to + * sleep until another entry needs to be updated. + */ +static struct task *ssl_ocsp_update_responses(struct task *task, void *context, unsigned int state) +{ + unsigned int next_wakeup = 0; + struct eb64_node *eb; + struct certificate_ocsp *ocsp; + struct httpclient *hc = NULL; + struct buffer *req_url = NULL; + struct buffer *req_body = NULL; + OCSP_CERTID *certid = NULL; + struct ssl_ocsp_task_ctx *ctx = &ssl_ocsp_task_ctx; + + if (ctx->cur_ocsp) { + /* An update is in process */ + ocsp = ctx->cur_ocsp; + hc = ctx->hc; + if (ctx->flags & HC_F_RES_STLINE) { + if (hc->res.status != 200) { + ctx->update_status = OCSP_UPDT_ERR_HTTP_STATUS; + goto http_error; + } + ctx->flags &= ~HC_F_RES_STLINE; + } + + if (ctx->flags & HC_F_RES_HDR) { + struct http_hdr *hdr; + int found = 0; + /* Look for "Content-Type" header which should have + * "application/ocsp-response" value. */ + for (hdr = hc->res.hdrs; isttest(hdr->v); hdr++) { + if (isteqi(hdr->n, ist("Content-Type")) && + isteqi(hdr->v, ist("application/ocsp-response"))) { + found = 1; + break; + } + } + if (!found) { + ctx->update_status = OCSP_UPDT_ERR_HTTP_HDR; + goto http_error; + } + ctx->flags &= ~HC_F_RES_HDR; + } + + /* If the HC_F_RES_BODY is set, we still need for the + * HC_F_RES_END flag to be set as well in order to be sure that + * the body is complete. */ + + /* we must close only if F_RES_END is the last flag */ + if (ctx->flags & HC_F_RES_END) { + + /* Process the body that must be complete since + * HC_F_RES_END is set. */ + if (ctx->flags & HC_F_RES_BODY) { + if (ssl_ocsp_check_response(ocsp->chain, ocsp->issuer, &hc->res.buf, NULL)) { + ctx->update_status = OCSP_UPDT_ERR_CHECK; + goto http_error; + } + + if (ssl_sock_update_ocsp_response(&hc->res.buf, NULL) != 0) { + ctx->update_status = OCSP_UPDT_ERR_INSERT; + goto http_error; + } + + ctx->flags &= ~HC_F_RES_BODY; + } + + ctx->flags &= ~HC_F_RES_END; + + ++ocsp->num_success; + ocsp->last_update = date.tv_sec; + ctx->update_status = OCSP_UPDT_OK; + ocsp->last_update_status = ctx->update_status; + + ssl_ocsp_send_log(); + + /* Reinsert the entry into the update list so that it can be updated later */ + ssl_ocsp_update_insert(ocsp); + /* Release the reference kept on the updated ocsp response. */ + ssl_sock_free_ocsp_instance(ctx->cur_ocsp); + ctx->cur_ocsp = NULL; + + HA_SPIN_LOCK(OCSP_LOCK, &ocsp_tree_lock); + /* Set next_wakeup to the new first entry of the tree */ + eb = eb64_first(&ocsp_update_tree); + if (eb) { + if (eb->key > date.tv_sec) + next_wakeup = (eb->key - date.tv_sec)*1000; + else + next_wakeup = 0; + } + HA_SPIN_UNLOCK(OCSP_LOCK, &ocsp_tree_lock); + goto leave; + } + + /* We did not receive the HC_F_RES_END flag yet, wait for it + * before trying to update a new ocsp response. */ + goto wait; + } else { + /* Look for next entry that needs to be updated. */ + const unsigned char *p = NULL; + + HA_SPIN_LOCK(OCSP_LOCK, &ocsp_tree_lock); + + eb = eb64_first(&ocsp_update_tree); + if (!eb) { + HA_SPIN_UNLOCK(OCSP_LOCK, &ocsp_tree_lock); + goto wait; + } + + if (eb->key > date.tv_sec) { + next_wakeup = (eb->key - date.tv_sec)*1000; + HA_SPIN_UNLOCK(OCSP_LOCK, &ocsp_tree_lock); + goto leave; + } + + ocsp = eb64_entry(eb, struct certificate_ocsp, next_update); + + /* Take the current entry out of the update tree, it will be + * reinserted after the response is processed. */ + eb64_delete(&ocsp->next_update); + + ocsp->updating = 1; + ocsp->refcount_instance++; + ctx->cur_ocsp = ocsp; + ocsp->last_update_status = OCSP_UPDT_UNKNOWN; + + HA_SPIN_UNLOCK(OCSP_LOCK, &ocsp_tree_lock); + + req_url = alloc_trash_chunk(); + if (!req_url) { + goto leave; + } + req_body = alloc_trash_chunk(); + if (!req_body) { + goto leave; + } + + p = ocsp->key_data; + + d2i_OCSP_CERTID(&certid, &p, ocsp->key_length); + if (!certid) + goto leave; + + /* Copy OCSP URI stored in ocsp structure into req_url */ + chunk_cpy(req_url, ocsp->uri); + + /* Create ocsp request */ + if (ssl_ocsp_create_request_details(certid, req_url, req_body, NULL) != 0) { + goto leave; + } + + /* Depending on the processing that occurred in + * ssl_ocsp_create_request_details we could either have to send + * a GET or a POST request. */ + hc = httpclient_new_from_proxy(httpclient_ocsp_update_px, task, + b_data(req_body) ? HTTP_METH_POST : HTTP_METH_GET, + ist2(b_orig(req_url), b_data(req_url))); + if (!hc) { + goto leave; + } + + if (httpclient_req_gen(hc, hc->req.url, hc->req.meth, + b_data(req_body) ? ocsp_request_hdrs : NULL, + b_data(req_body) ? ist2(b_orig(req_body), b_data(req_body)) : IST_NULL) != ERR_NONE) { + goto leave; + } + + hc->ops.res_stline = ocsp_update_response_stline_cb; + hc->ops.res_headers = ocsp_update_response_headers_cb; + hc->ops.res_payload = ocsp_update_response_body_cb; + hc->ops.res_end = ocsp_update_response_end_cb; + + if (!(ctx->appctx = httpclient_start(hc))) { + goto leave; + } + + ctx->flags = 0; + ctx->hc = hc; + + /* We keep the lock, this indicates that an update is in process. */ + goto wait; + } + +leave: + if (ctx->cur_ocsp) { + /* Something went wrong, reinsert the entry in the tree. */ + ++ctx->cur_ocsp->num_failure; + ssl_ocsp_update_insert_after_error(ctx->cur_ocsp); + /* Release the reference kept on the updated ocsp response. */ + ssl_sock_free_ocsp_instance(ctx->cur_ocsp); + ctx->cur_ocsp = NULL; + } + if (hc) + httpclient_stop_and_destroy(hc); + ctx->hc = NULL; + free_trash_chunk(req_url); + free_trash_chunk(req_body); + task->expire = tick_add(now_ms, next_wakeup); + return task; + +wait: + free_trash_chunk(req_url); + free_trash_chunk(req_body); + task->expire = TICK_ETERNITY; + return task; + +http_error: + ssl_ocsp_send_log(); + /* Reinsert certificate into update list so that it can be updated later */ + if (ocsp) { + ++ocsp->num_failure; + ocsp->last_update_status = ctx->update_status; + ssl_ocsp_update_insert_after_error(ocsp); + } + + if (hc) + httpclient_stop_and_destroy(hc); + /* Release the reference kept on the updated ocsp response. */ + ssl_sock_free_ocsp_instance(ctx->cur_ocsp); + HA_SPIN_LOCK(OCSP_LOCK, &ocsp_tree_lock); + /* Set next_wakeup to the new first entry of the tree */ + eb = eb64_first(&ocsp_update_tree); + if (eb) { + if (eb->key > date.tv_sec) + next_wakeup = (eb->key - date.tv_sec)*1000; + else + next_wakeup = 0; + } + HA_SPIN_UNLOCK(OCSP_LOCK, &ocsp_tree_lock); + ctx->cur_ocsp = NULL; + ctx->hc = NULL; + ctx->flags = 0; + task->expire = tick_add(now_ms, next_wakeup); + return task; +} + +char ocspupdate_log_format[] = "%ci:%cp [%tr] %ft %[ssl_ocsp_certname] %[ssl_ocsp_status] %{+Q}[ssl_ocsp_status_str] %[ssl_ocsp_fail_cnt] %[ssl_ocsp_success_cnt]"; + +/* + * Initialize the proxy for the OCSP update HTTP client with 2 servers, one for + * raw HTTP, the other for HTTPS. + */ +static int ssl_ocsp_update_precheck() +{ + /* initialize the OCSP update dedicated httpclient */ + httpclient_ocsp_update_px = httpclient_create_proxy("<OCSP-UPDATE>"); + if (!httpclient_ocsp_update_px) + return 1; + httpclient_ocsp_update_px->conf.error_logformat_string = strdup(ocspupdate_log_format); + httpclient_ocsp_update_px->conf.logformat_string = httpclient_log_format; + httpclient_ocsp_update_px->options2 |= PR_O2_NOLOGNORM; + + return 0; +} + +/* initialize the proxy and servers for the HTTP client */ + +REGISTER_PRE_CHECK(ssl_ocsp_update_precheck); + + +static int cli_parse_update_ocsp_response(char **args, char *payload, struct appctx *appctx, void *private) +{ + char *err = NULL; + struct ckch_store *ckch_store = NULL; + struct certificate_ocsp *ocsp = NULL; + int update_once = 0; + unsigned char key[OCSP_MAX_CERTID_ASN1_LENGTH] = {}; + unsigned char *p; + + if (!*args[3]) { + memprintf(&err, "'update ssl ocsp-response' expects a filename\n"); + return cli_dynerr(appctx, err); + } + + /* The operations on the CKCH architecture are locked so we can + * manipulate ckch_store and ckch_inst */ + if (HA_SPIN_TRYLOCK(CKCH_LOCK, &ckch_lock)) { + memprintf(&err, "%sCan't update the certificate!\nOperations on certificates are currently locked!\n", err ? err : ""); + goto end; + } + + ckch_store = ckchs_lookup(args[3]); + + if (!ckch_store) { + memprintf(&err, "%sUnknown certificate! 'update ssl ocsp-response' expects an already known certificate file name.\n", err ? err : ""); + HA_SPIN_UNLOCK(CKCH_LOCK, &ckch_lock); + goto end; + } + + p = key; + i2d_OCSP_CERTID(ckch_store->data->ocsp_cid, &p); + + HA_SPIN_UNLOCK(CKCH_LOCK, &ckch_lock); + + + HA_SPIN_LOCK(OCSP_LOCK, &ocsp_tree_lock); + ocsp = (struct certificate_ocsp *)ebmb_lookup(&cert_ocsp_tree, key, OCSP_MAX_CERTID_ASN1_LENGTH); + if (!ocsp) { + memprintf(&err, "%s'update ssl ocsp-response' only works on certificates that already have a known OCSP response.\n", err ? err : ""); + HA_SPIN_UNLOCK(OCSP_LOCK, &ocsp_tree_lock); + goto end; + } + + /* No need to try to update this response, it is already being updated. */ + if (!ocsp->updating) { + update_once = (ocsp->next_update.node.leaf_p == NULL); + eb64_delete(&ocsp->next_update); + + /* Insert the entry at the beginning of the update tree. + * We don't need to increase the reference counter on the + * certificate_ocsp structure because we would not have a way to + * decrease it afterwards since this update operation is asynchronous. + * If the corresponding entry were to be destroyed before the update can + * be performed, which is pretty unlikely, it would not be such a + * problem because that would mean that the OCSP response is not + * actually used. + */ + ocsp->next_update.key = 0; + eb64_insert(&ocsp_update_tree, &ocsp->next_update); + ocsp->update_once = update_once; + } + + HA_SPIN_UNLOCK(OCSP_LOCK, &ocsp_tree_lock); + + if (!ocsp_update_task) + ssl_create_ocsp_update_task(&err); + + task_wakeup(ocsp_update_task, TASK_WOKEN_MSG); + + free(err); + + return 0; + +end: + return cli_dynerr(appctx, memprintf(&err, "%sCan't send ocsp request for %s!\n", err ? err : "", args[3])); +} + +#endif /* !defined OPENSSL_IS_BORINGSSL */ + + +#endif /* (defined SSL_CTRL_SET_TLSEXT_STATUS_REQ_CB && !defined OPENSSL_NO_OCSP) */ + + +static int cli_parse_set_ocspresponse(char **args, char *payload, struct appctx *appctx, void *private) +{ +#if (defined SSL_CTRL_SET_TLSEXT_STATUS_REQ_CB && !defined OPENSSL_NO_OCSP) + char *err = NULL; + int i, j, ret; + + if (!payload) + payload = args[3]; + + /* Expect one parameter: the new response in base64 encoding */ + if (!*payload) + return cli_err(appctx, "'set ssl ocsp-response' expects response in base64 encoding.\n"); + + /* remove \r and \n from the payload */ + for (i = 0, j = 0; payload[i]; i++) { + if (payload[i] == '\r' || payload[i] == '\n') + continue; + payload[j++] = payload[i]; + } + payload[j] = 0; + + ret = base64dec(payload, j, trash.area, trash.size); + if (ret < 0) + return cli_err(appctx, "'set ssl ocsp-response' received invalid base64 encoded response.\n"); + + trash.data = ret; + if (ssl_sock_update_ocsp_response(&trash, &err)) { + if (err) + return cli_dynerr(appctx, memprintf(&err, "%s.\n", err)); + else + return cli_err(appctx, "Failed to update OCSP response.\n"); + } + + return cli_msg(appctx, LOG_INFO, "OCSP Response updated!\n"); +#else + return cli_err(appctx, "HAProxy was compiled against a version of OpenSSL that doesn't support OCSP stapling.\n"); +#endif + +} + +/* parsing function for 'show ssl ocsp-response [id]'. If an entry is forced, + * it's set into appctx->svcctx. + */ +static int cli_parse_show_ocspresponse(char **args, char *payload, struct appctx *appctx, void *private) +{ +#if ((defined SSL_CTRL_SET_TLSEXT_STATUS_REQ_CB && !defined OPENSSL_NO_OCSP) && !defined OPENSSL_IS_BORINGSSL) + + struct show_ocspresp_cli_ctx *ctx = applet_reserve_svcctx(appctx, sizeof(*ctx)); + int arg_idx = 3; + + if (*args[3]) { + struct certificate_ocsp *ocsp = NULL; + char key[OCSP_MAX_CERTID_ASN1_LENGTH] = {}; + int key_length = OCSP_MAX_CERTID_ASN1_LENGTH; + char *key_ptr = key; + unsigned char *p; + struct ckch_store *ckch_store = NULL; + + if (strcmp(args[3], "text") == 0) { + ctx->format = SHOW_OCSPRESP_FMT_TEXT; + ++arg_idx; + } else if (strcmp(args[3], "base64") == 0) { + ctx->format = SHOW_OCSPRESP_FMT_B64; + ++arg_idx; + } + + if (ctx->format != SHOW_OCSPRESP_FMT_DFLT && !*args[arg_idx]) + return cli_err(appctx, "'show ssl ocsp-response [text|base64]' expects a valid certid.\n"); + + /* Try to convert parameter into an OCSP certid first, and consider it + * as a filename if it fails. */ + if (strlen(args[arg_idx]) > OCSP_MAX_CERTID_ASN1_LENGTH*2 || + !parse_binary(args[arg_idx], &key_ptr, &key_length, NULL)) { + + key_ptr = key; + key_length = 0; + + /* The operations on the CKCH architecture are locked so we can + * manipulate ckch_store and ckch_inst */ + if (HA_SPIN_TRYLOCK(CKCH_LOCK, &ckch_lock)) { + return cli_err(appctx, "Operations on certificates are currently locked!\n"); + } + + ckch_store = ckchs_lookup(args[arg_idx]); + + if (ckch_store) { + p = (unsigned char*)key; + key_length = i2d_OCSP_CERTID(ckch_store->data->ocsp_cid, &p); + } + HA_SPIN_UNLOCK(CKCH_LOCK, &ckch_lock); + } + + if (key_length == 0) { + return cli_err(appctx, "'show ssl ocsp-response' expects a valid certid or certificate path.\n"); + } + + HA_SPIN_LOCK(OCSP_LOCK, &ocsp_tree_lock); + ocsp = (struct certificate_ocsp *)ebmb_lookup(&cert_ocsp_tree, key, OCSP_MAX_CERTID_ASN1_LENGTH); + + if (!ocsp) { + HA_SPIN_UNLOCK(OCSP_LOCK, &ocsp_tree_lock); + return cli_err(appctx, "Certificate ID or path does not match any certificate.\n"); + } + ocsp->refcount_instance++; + HA_SPIN_UNLOCK(OCSP_LOCK, &ocsp_tree_lock); + + ctx->ocsp = ocsp; + appctx->io_handler = cli_io_handler_show_ocspresponse_detail; + } + + return 0; + +#else + return cli_err(appctx, "HAProxy was compiled against a version of OpenSSL that doesn't support OCSP stapling.\n"); +#endif +} + +/* + * IO handler of "show ssl ocsp-response". The command taking a specific ID + * is managed in cli_io_handler_show_ocspresponse_detail. + * The current entry is taken from appctx->svcctx. + */ +static int cli_io_handler_show_ocspresponse(struct appctx *appctx) +{ +#if ((defined SSL_CTRL_SET_TLSEXT_STATUS_REQ_CB && !defined OPENSSL_NO_OCSP) && !defined OPENSSL_IS_BORINGSSL) + struct buffer *trash = alloc_trash_chunk(); + struct buffer *tmp = NULL; + struct ebmb_node *node; + struct certificate_ocsp *ocsp = NULL; + BIO *bio = NULL; + int write = -1; + struct show_ocspresp_cli_ctx *ctx = appctx->svcctx; + + if (trash == NULL) + return 1; + + HA_SPIN_LOCK(OCSP_LOCK, &ocsp_tree_lock); + + tmp = alloc_trash_chunk(); + if (!tmp) + goto end; + + if ((bio = BIO_new(BIO_s_mem())) == NULL) + goto end; + + if (!ctx->ocsp) { + chunk_appendf(trash, "# Certificate IDs\n"); + node = ebmb_first(&cert_ocsp_tree); + } else { + node = &ctx->ocsp->key; + } + + while (node) { + OCSP_CERTID *certid = NULL; + const unsigned char *p = NULL; + int i; + + ocsp = ebmb_entry(node, struct certificate_ocsp, key); + + /* Dump the key in hexadecimal */ + chunk_appendf(trash, "Certificate ID key : "); + for (i = 0; i < ocsp->key_length; ++i) { + chunk_appendf(trash, "%02x", ocsp->key_data[i]); + } + chunk_appendf(trash, "\n"); + + /* Dump the certificate path */ + chunk_appendf(trash, "Certificate path : %s\n", ocsp->path); + + p = ocsp->key_data; + + /* Decode the certificate ID (serialized into the key). */ + d2i_OCSP_CERTID(&certid, &p, ocsp->key_length); + if (!certid) + goto end; + + /* Dump the CERTID info */ + ocsp_certid_print(bio, certid, 1); + OCSP_CERTID_free(certid); + write = BIO_read(bio, tmp->area, tmp->size-1); + /* strip trailing LFs */ + while (write > 0 && tmp->area[write-1] == '\n') + write--; + tmp->area[write] = '\0'; + + chunk_appendf(trash, "%s\n", tmp->area); + + node = ebmb_next(node); + if (applet_putchk(appctx, trash) == -1) + goto yield; + } + +end: + HA_SPIN_UNLOCK(OCSP_LOCK, &ocsp_tree_lock); + free_trash_chunk(trash); + free_trash_chunk(tmp); + BIO_free(bio); + return 1; + +yield: + free_trash_chunk(trash); + free_trash_chunk(tmp); + BIO_free(bio); + + ocsp->refcount_instance++; + ctx->ocsp = ocsp; + HA_SPIN_UNLOCK(OCSP_LOCK, &ocsp_tree_lock); + return 0; +#else + return cli_err(appctx, "HAProxy was compiled against a version of OpenSSL that doesn't support OCSP stapling.\n"); +#endif +} + +static void cli_release_show_ocspresponse(struct appctx *appctx) +{ + struct show_ocspresp_cli_ctx *ctx = appctx->svcctx; + + if (ctx) + ssl_sock_free_ocsp(ctx->ocsp); +} + +/* Check if the ckch_store and the entry does have the same configuration */ +int ocsp_update_check_cfg_consistency(struct ckch_store *store, struct crtlist_entry *entry, char *crt_path, char **err) +{ + int err_code = ERR_NONE; + + if (store->data->ocsp_update_mode != SSL_SOCK_OCSP_UPDATE_DFLT || entry->ssl_conf) { + if ((!entry->ssl_conf && store->data->ocsp_update_mode == SSL_SOCK_OCSP_UPDATE_ON) + || (entry->ssl_conf && store->data->ocsp_update_mode != entry->ssl_conf->ocsp_update)) { + memprintf(err, "%sIncompatibilities found in OCSP update mode for certificate %s\n", err && *err ? *err : "", crt_path); + err_code |= ERR_ALERT | ERR_FATAL; + } + } + return err_code; +} + +struct show_ocsp_updates_ctx { + struct certificate_ocsp *cur_ocsp; +}; + +/* + * Parsing function for 'show ssl ocsp-updates [nb]'. + */ +static int cli_parse_show_ocsp_updates(char **args, char *payload, struct appctx *appctx, void *private) +{ +#if ((defined SSL_CTRL_SET_TLSEXT_STATUS_REQ_CB && !defined OPENSSL_NO_OCSP) && !defined OPENSSL_IS_BORINGSSL) + struct show_ocsp_updates_ctx *ctx = applet_reserve_svcctx(appctx, sizeof(*ctx)); + + HA_SPIN_LOCK(OCSP_LOCK, &ocsp_tree_lock); + + return 0; +#else + return cli_err(appctx, "HAProxy was compiled against a version of OpenSSL that doesn't support OCSP stapling.\n"); +#endif +} + +/* + * Dump information about an ocsp response concerning ocsp auto update. + * It follows the following format : + * OCSP Certid | Path | Next Update | Last Update | Successes | Failures | Last Update Status | Last Update Status (str) + * Return 0 in case of success. + */ +static int dump_ocsp_update_info(struct certificate_ocsp *ocsp, struct buffer *out) +{ + struct tm tm = {}; + char *ret; + int i; + time_t next_update; + + /* Dump OCSP certid */ + for (i = 0; i < ocsp->key_length; ++i) { + chunk_appendf(out, "%02x", ocsp->key_data[i]); + } + + chunk_appendf(out, " | "); + + /* Dump path */ + chunk_appendf(out, "%s", ocsp->path); + + chunk_appendf(out, " | "); + + /* Dump next update time */ + if (ocsp->next_update.key != 0) { + next_update = ocsp->next_update.key; + get_localtime(ocsp->next_update.key, &tm); + } else { + next_update = date.tv_sec; + get_localtime(date.tv_sec, &tm); + } + ret = localdate2str_log(b_orig(out)+b_data(out), next_update, &tm, b_size(out)-b_data(out)); + + if (ret == NULL) + return 1; + + out->data = (ret - out->area); + + chunk_appendf(out, " | "); + + /* Dump last update time or "-" if no update occurred yet */ + if (ocsp->last_update) { + get_localtime(ocsp->last_update, &tm); + ret = localdate2str_log(b_orig(out)+b_data(out), ocsp->last_update, &tm, b_size(out)-b_data(out)); + + if (ret == NULL) + return 1; + + out->data = (ret - out->area); + } else + chunk_appendf(out, "-"); + + chunk_appendf(out, " | "); + + /* Number of successful updates */ + chunk_appendf(out, "%d", ocsp->num_success); + + chunk_appendf(out, " | "); + + /* Number of failed updates */ + chunk_appendf(out, "%d", ocsp->num_failure); + + chunk_appendf(out, " | "); + + /* Last update status */ + chunk_appendf(out, "%d", ocsp->last_update_status); + + chunk_appendf(out, " | "); + + /* Last update status str */ + if (ocsp->last_update_status >= OCSP_UPDT_ERR_LAST) + chunk_appendf(out, "-"); + else + chunk_appendf(out, "%s", istptr(ocsp_update_errors[ocsp->last_update_status])); + + chunk_appendf(out, "\n"); + + return 0; +} + +static int cli_io_handler_show_ocsp_updates(struct appctx *appctx) +{ + struct show_ocsp_updates_ctx *ctx = appctx->svcctx; + struct eb64_node *node; + struct certificate_ocsp *ocsp = NULL; + struct buffer *trash = get_trash_chunk(); + + if (!ctx->cur_ocsp) { + node = eb64_first(&ocsp_update_tree); + chunk_appendf(trash, "OCSP Certid | Path | Next Update | Last Update | Successes | Failures | Last Update Status | Last Update Status (str)\n"); + + /* Look for an entry currently being updated */ + ocsp = ssl_ocsp_task_ctx.cur_ocsp; + if (ocsp) { + if (dump_ocsp_update_info(ocsp, trash)) + goto end; + } + + if (applet_putchk(appctx, trash) == -1) + goto yield; + + } else { + node = &((struct certificate_ocsp*)ctx->cur_ocsp)->next_update; + } + + while (node) { + ocsp = eb64_entry(node, struct certificate_ocsp, next_update); + + chunk_reset(trash); + if (dump_ocsp_update_info(ocsp, trash)) + goto end; + + if (applet_putchk(appctx, trash) == -1) { + ctx->cur_ocsp = ocsp; + goto yield; + } + + node = eb64_next(node); + } + +end: + return 1; + +yield: + return 0; /* should come back */ +} + +static void cli_release_show_ocsp_updates(struct appctx *appctx) +{ + HA_SPIN_UNLOCK(OCSP_LOCK, &ocsp_tree_lock); +} + + +static int +smp_fetch_ssl_ocsp_certid(const struct arg *args, struct sample *smp, const char *kw, void *private) +{ + struct buffer *data = get_trash_chunk(); + struct certificate_ocsp *ocsp = ssl_ocsp_task_ctx.cur_ocsp; + + if (!ocsp) + return 0; + + dump_binary(data, (char *)ocsp->key_data, ocsp->key_length); + + smp->data.type = SMP_T_STR; + smp->data.u.str = *data; + return 1; +} + +static int +smp_fetch_ssl_ocsp_certname(const struct arg *args, struct sample *smp, const char *kw, void *private) +{ + struct certificate_ocsp *ocsp = ssl_ocsp_task_ctx.cur_ocsp; + + if (!ocsp) + return 0; + + smp->data.type = SMP_T_STR; + smp->data.u.str.area = ocsp->path; + smp->data.u.str.data = strlen(ocsp->path); + return 1; +} + +static int +smp_fetch_ssl_ocsp_status(const struct arg *args, struct sample *smp, const char *kw, void *private) +{ + struct certificate_ocsp *ocsp = ssl_ocsp_task_ctx.cur_ocsp; + + if (!ocsp) + return 0; + + smp->data.type = SMP_T_SINT; + smp->data.u.sint = ssl_ocsp_task_ctx.update_status; + return 1; +} + +static int +smp_fetch_ssl_ocsp_status_str(const struct arg *args, struct sample *smp, const char *kw, void *private) +{ + struct certificate_ocsp *ocsp = ssl_ocsp_task_ctx.cur_ocsp; + + if (!ocsp) + return 0; + + if (ssl_ocsp_task_ctx.update_status >= OCSP_UPDT_ERR_LAST) + return 0; + + smp->data.type = SMP_T_STR; + smp->data.u.str = ist2buf(ocsp_update_errors[ssl_ocsp_task_ctx.update_status]); + + return 1; +} + +static int +smp_fetch_ssl_ocsp_fail_cnt(const struct arg *args, struct sample *smp, const char *kw, void *private) +{ + struct certificate_ocsp *ocsp = ssl_ocsp_task_ctx.cur_ocsp; + + if (!ocsp) + return 0; + + smp->data.type = SMP_T_SINT; + smp->data.u.sint = ocsp->num_failure; + return 1; +} + +static int +smp_fetch_ssl_ocsp_success_cnt(const struct arg *args, struct sample *smp, const char *kw, void *private) +{ + struct certificate_ocsp *ocsp = ssl_ocsp_task_ctx.cur_ocsp; + + if (!ocsp) + return 0; + + smp->data.type = SMP_T_SINT; + smp->data.u.sint = ocsp->num_success; + return 1; +} + + +static struct cli_kw_list cli_kws = {{ },{ + { { "set", "ssl", "ocsp-response", NULL }, "set ssl ocsp-response <resp|payload> : update a certificate's OCSP Response from a base64-encode DER", cli_parse_set_ocspresponse, NULL }, + + { { "show", "ssl", "ocsp-response", NULL },"show ssl ocsp-response [[text|base64] id] : display the IDs of the OCSP responses used in memory, or the details of a single OCSP response (in text or base64 format)", cli_parse_show_ocspresponse, cli_io_handler_show_ocspresponse, cli_release_show_ocspresponse }, + { { "show", "ssl", "ocsp-updates", NULL }, "show ssl ocsp-updates : display information about the next 'nb' ocsp responses that will be updated automatically", cli_parse_show_ocsp_updates, cli_io_handler_show_ocsp_updates, cli_release_show_ocsp_updates }, +#if ((defined SSL_CTRL_SET_TLSEXT_STATUS_REQ_CB && !defined OPENSSL_NO_OCSP) && !defined OPENSSL_IS_BORINGSSL) + { { "update", "ssl", "ocsp-response", NULL }, "update ssl ocsp-response <certfile> : send ocsp request and update stored ocsp response", cli_parse_update_ocsp_response, NULL, NULL }, +#endif + { { NULL }, NULL, NULL, NULL } +}}; + +INITCALL1(STG_REGISTER, cli_register_kw, &cli_kws); + + +/* Note: must not be declared <const> as its list will be overwritten. + * Please take care of keeping this list alphabetically sorted. + * + * Those fetches only have a valid value during an OCSP update process so they + * can only be used in a log format of a log line built by the update process + * task itself. + */ +static struct sample_fetch_kw_list sample_fetch_keywords = {ILH, { + { "ssl_ocsp_certid", smp_fetch_ssl_ocsp_certid, 0, NULL, SMP_T_STR, SMP_USE_L5SRV }, + { "ssl_ocsp_certname", smp_fetch_ssl_ocsp_certname, 0, NULL, SMP_T_STR, SMP_USE_L5SRV }, + { "ssl_ocsp_status", smp_fetch_ssl_ocsp_status, 0, NULL, SMP_T_SINT, SMP_USE_L5SRV }, + { "ssl_ocsp_status_str", smp_fetch_ssl_ocsp_status_str, 0, NULL, SMP_T_STR, SMP_USE_L5SRV }, + { "ssl_ocsp_fail_cnt", smp_fetch_ssl_ocsp_fail_cnt, 0, NULL, SMP_T_SINT, SMP_USE_L5SRV }, + { "ssl_ocsp_success_cnt", smp_fetch_ssl_ocsp_success_cnt, 0, NULL, SMP_T_SINT, SMP_USE_L5SRV }, + { NULL, NULL, 0, 0, 0 }, +}}; + +INITCALL1(STG_REGISTER, sample_register_fetches, &sample_fetch_keywords); + + +/* + * Local variables: + * c-indent-level: 8 + * c-basic-offset: 8 + * End: + */ |