]> git.ipfire.org Git - thirdparty/haproxy.git/commitdiff
MEDIUM: ssl: Handle early data with OpenSSL 1.1.1
authorOlivier Houchard <ohouchard@haproxy.com>
Fri, 22 Sep 2017 16:26:28 +0000 (18:26 +0200)
committerWilly Tarreau <w@1wt.eu>
Fri, 27 Oct 2017 08:54:05 +0000 (10:54 +0200)
When compiled with Openssl >= 1.1.1, before attempting to do the handshake,
try to read any early data. If any early data is present, then we'll create
the session, read the data, and handle the request before we're doing the
handshake.

For this, we add a new connection flag, CO_FL_EARLY_SSL_HS, which is not
part of the CO_FL_HANDSHAKE set, allowing to proceed with a session even
before an SSL handshake is completed.

As early data do have security implication, we let the origin server know
the request comes from early data by adding the "Early-Data" header, as
specified in this draft from the HTTP working group :

    https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-replay

doc/configuration.txt
include/proto/connection.h
include/types/connection.h
include/types/listener.h
src/proto_http.c
src/proto_tcp.c
src/session.c
src/ssl_sock.c

index bd8cafa23f069f0442822dfe49f5fb659c5a52ff..7ab0f3c59512c454f2cb31f20dca6f6c11038a17 100644 (file)
@@ -10237,6 +10237,10 @@ accept-proxy
   usable. See also "tcp-request connection expect-proxy" for a finer-grained
   setting of which client is allowed to use the protocol.
 
+allow-0rtt
+  Allow receiving early data when using TLS 1.3. This is disabled by default,
+  due to security considerations.
+
 alpn <protocols>
   This enables the TLS ALPN extension and advertises the specified protocol
   list as supported on top of ALPN. The protocol list consists in a comma-
index 0044d8185e0acab56047c69a63017c404c81fcb6..7060046b82f890d7b04ff1fc5b7e32926b8553c4 100644 (file)
@@ -494,6 +494,7 @@ static inline void conn_init(struct connection *conn)
        conn->obj_type = OBJ_TYPE_CONN;
        conn->flags = CO_FL_NONE;
        conn->data = NULL;
+       conn->tmp_early_data = -1;
        conn->owner = NULL;
        conn->send_proxy_ofs = 0;
        conn->handle.fd = DEAD_FD_MAGIC;
index c1560cb6d0920311cca6f4b480913c35435bba54..1c923c5784cdb58cb7ac654eacbe6c96aa92c2fb 100644 (file)
@@ -95,8 +95,8 @@ enum {
        CO_FL_ADDR_FROM_SET = 0x00001000,  /* addr.from is set */
        CO_FL_ADDR_TO_SET   = 0x00002000,  /* addr.to is set */
 
-       /* unused : 0x00004000 */
-       /* unused : 0x00008000 */
+       CO_FL_EARLY_SSL_HS  = 0x00004000,  /* We have early data pending, don't start SSL handshake yet */
+       CO_FL_EARLY_DATA    = 0x00008000,  /* At least some of the data are early data */
        /* unused : 0x00010000 */
        /* unused : 0x00020000 */
 
@@ -299,6 +299,7 @@ struct connection {
        const struct xprt_ops *xprt;  /* operations at the transport layer */
        const struct data_cb  *data;  /* data layer callbacks. Must be set before xprt->init() */
        void *xprt_ctx;               /* general purpose pointer, initialized to NULL */
+       int tmp_early_data;           /* 1st byte of early data, if any */
        void *owner;                  /* pointer to upper layer's entity (eg: session, stream interface) */
        int xprt_st;                  /* transport layer state, initialized to zero */
        union conn_handle handle;     /* connection handle at the socket layer */
index 3d9ad7f7b7422e2b02466f2be1718478ce64a307..19d1dbe3bb51d97fc711e80266d5a03c4fa216bf 100644 (file)
@@ -105,6 +105,7 @@ enum li_state {
 #define BC_SSL_O_NONE           0x0000
 #define BC_SSL_O_NO_TLS_TICKETS 0x0100 /* disable session resumption tickets */
 #define BC_SSL_O_PREF_CLIE_CIPH 0x0200  /* prefer client ciphers */
+#define BC_SSL_O_EARLY_DATA     0x0400  /* Accept early data */
 #endif
 
 /* ssl "bind" settings */
@@ -120,6 +121,7 @@ struct ssl_bind_conf {
 #endif
        int verify:3;              /* verify method (set of SSL_VERIFY_* flags) */
        int no_ca_names:1;         /* do not send ca names to clients (ca_file related) */
+       int early_data:1;          /* early data allowed */
        char *ca_file;             /* CAfile to use on verify */
        char *crl_file;            /* CRLfile to use on verify */
        char *ciphers;             /* cipher suite to use if non-null */
index 939a7a137f786ede31447796ffc39b0bb9a3dc3b..e632ce5fe551e236b1bd64cf7444b5eef568f53f 100644 (file)
@@ -3341,6 +3341,7 @@ int http_process_req_common(struct stream *s, struct channel *req, int an_bit, s
        struct cond_wordlist *wl;
        enum rule_result verdict;
        int deny_status = HTTP_ERR_403;
+       struct connection *conn = objt_conn(sess->origin);
 
        if (unlikely(msg->msg_state < HTTP_MSG_BODY)) {
                /* we need more data */
@@ -3387,6 +3388,21 @@ int http_process_req_common(struct stream *s, struct channel *req, int an_bit, s
                }
        }
 
+       if (conn && conn->flags & CO_FL_EARLY_DATA) {
+               struct hdr_ctx ctx;
+
+               ctx.idx = 0;
+               if (!http_find_header2("Early-Data", strlen("Early-Data"),
+                   s->req.buf->p, &txn->hdr_idx, &ctx)) {
+                       if (unlikely(http_header_add_tail2(&txn->req,
+                           &txn->hdr_idx, "Early-Data: 1",
+                           strlen("Early-Data: 1"))) < 0) {
+                               goto return_bad_req;
+                        }
+               }
+
+       }
+
        /* OK at this stage, we know that the request was accepted according to
         * the http-request rules, we can check for the stats. Note that the
         * URI is detected *before* the req* rules in order not to be affected
index e4e6483db00d3367237fe33dd3b1cd6302563efb..b43fdef59ba39f676a1521a207468c3aedaad63d 100644 (file)
@@ -550,8 +550,10 @@ int tcp_connect_server(struct connection *conn, int data, int delack)
                return SF_ERR_RESOURCE;
        }
 
-       if (conn->flags & (CO_FL_HANDSHAKE | CO_FL_WAIT_L4_CONN)) {
+       if (conn->flags & (CO_FL_HANDSHAKE | CO_FL_WAIT_L4_CONN | CO_FL_EARLY_SSL_HS)) {
                conn_sock_want_send(conn);  /* for connect status, proxy protocol or SSL */
+               if (conn->flags & CO_FL_EARLY_SSL_HS)
+                       conn_xprt_want_send(conn);
        }
        else {
                /* If there's no more handshake, we need to notify the data
index bc0b6d643125f4367baf6eebbde768ec6aa56b2c..ecfa2f14d9a0c44844d7676800c4c36d0b2b0c19 100644 (file)
@@ -240,7 +240,7 @@ int session_accept_fd(struct listener *l, int cfd, struct sockaddr_storage *addr
         *           v       |           |        |
         *          conn -- owner ---> task <-----+
         */
-       if (cli_conn->flags & CO_FL_HANDSHAKE) {
+       if (cli_conn->flags & (CO_FL_HANDSHAKE | CO_FL_EARLY_SSL_HS)) {
                if (unlikely((sess->task = task_new()) == NULL))
                        goto out_free_sess;
 
index fceccc9ba7f2a6078e637c6de3fae8d5e4455dbb..579a25b3bf782b759d999b945a205407c284dba3 100644 (file)
@@ -1314,7 +1314,7 @@ void ssl_sock_infocbk(const SSL *ssl, int where, int ret)
 
        if (where & SSL_CB_HANDSHAKE_START) {
                /* Disable renegotiation (CVE-2009-3555) */
-               if (conn->flags & CO_FL_CONNECTED) {
+               if ((conn->flags & (CO_FL_CONNECTED | CO_FL_EARLY_SSL_HS)) == CO_FL_CONNECTED) {
                        conn->flags |= CO_FL_ERROR;
                        conn->err_code = CO_ER_SSL_RENEG;
                }
@@ -2002,11 +2002,14 @@ static int ssl_sock_switchctx_cbk(SSL *ssl, int *al, void *arg)
        const uint8_t *servername;
        size_t servername_len;
        struct ebmb_node *node, *n, *node_ecdsa = NULL, *node_rsa = NULL, *node_anonymous = NULL;
+       int allow_early = 0;
        int i;
 
        conn = SSL_get_app_data(ssl);
        s = objt_listener(conn->target)->bind_conf;
 
+       if (s->ssl_options & BC_SSL_O_EARLY_DATA)
+               allow_early = 1;
 #ifdef OPENSSL_IS_BORINGSSL
        if (SSL_early_callback_ctx_extension_get(ctx, TLSEXT_TYPE_server_name,
                                                 &extension_data, &extension_len)) {
@@ -2045,13 +2048,13 @@ static int ssl_sock_switchctx_cbk(SSL *ssl, int *al, void *arg)
        } else {
 #if (!defined SSL_NO_GENERATE_CERTIFICATES)
                if (s->generate_certs && ssl_sock_generate_certificate_from_conn(s, ssl)) {
-                       return 1;
+                       goto allow_early;
                }
 #endif
                /* without SNI extension, is the default_ctx (need SSL_TLSEXT_ERR_NOACK) */
                if (!s->strict_sni) {
                        ssl_sock_switchctx_set(ssl, s->default_ctx);
-                       return 1;
+                       goto allow_early;
                }
                goto abort;
        }
@@ -2189,19 +2192,29 @@ static int ssl_sock_switchctx_cbk(SSL *ssl, int *al, void *arg)
                ssl_sock_switchctx_set(ssl, container_of(node, struct sni_ctx, name)->ctx);
                methodVersions[conf->ssl_methods.min].ssl_set_version(ssl, SET_MIN);
                methodVersions[conf->ssl_methods.max].ssl_set_version(ssl, SET_MAX);
-               return 1;
+               if (conf->early_data)
+                       allow_early = 1;
+               goto allow_early;
        }
 #if (!defined SSL_NO_GENERATE_CERTIFICATES)
        if (s->generate_certs && ssl_sock_generate_certificate(trash.str, s, ssl)) {
                /* switch ctx done in ssl_sock_generate_certificate */
-               return 1;
+               goto allow_early;
        }
 #endif
        if (!s->strict_sni) {
                /* no certificate match, is the default_ctx */
                ssl_sock_switchctx_set(ssl, s->default_ctx);
-               return 1;
        }
+allow_early:
+#ifdef OPENSSL_IS_BORINGSSL
+       if (allow_early)
+               SSL_set_early_data_enabled(ssl, 1);
+#else
+       if (!allow_early)
+               SSL_set_max_early_data(ssl, 0);
+#endif
+       return 1;
  abort:
        /* abort handshake (was SSL_TLSEXT_ERR_ALERT_FATAL) */
        conn->err_code = CO_ER_SSL_HANDSHAKE;
@@ -3911,8 +3924,20 @@ int ssl_sock_prepare_ctx(struct bind_conf *bind_conf, struct ssl_bind_conf *ssl_
        if (!conf_curves) {
                int i;
                EC_KEY  *ecdh;
+#if (OPENSSL_VERSION_NUMBER >= 0x10101000L)
                const char *ecdhe = (ssl_conf && ssl_conf->ecdhe) ? ssl_conf->ecdhe :
-                       (bind_conf->ssl_conf.ecdhe ? bind_conf->ssl_conf.ecdhe : ECDHE_DEFAULT_CURVE);
+                       (bind_conf->ssl_conf.ecdhe ? bind_conf->ssl_conf.ecdhe :
+                        NULL);
+
+               if (ecdhe == NULL) {
+                       SSL_CTX_set_dh_auto(ctx, 1);
+                       return cfgerr;
+               }
+#else
+               const char *ecdhe = (ssl_conf && ssl_conf->ecdhe) ? ssl_conf->ecdhe :
+                       (bind_conf->ssl_conf.ecdhe ? bind_conf->ssl_conf.ecdhe :
+                        ECDHE_DEFAULT_CURVE);
+#endif
 
                i = OBJ_sn2nid(ecdhe);
                if (!i || ((ecdh = EC_KEY_new_by_curve_name(i)) == NULL)) {
@@ -4627,6 +4652,9 @@ static int ssl_sock_init(struct connection *conn)
 
                /* leave init state and start handshake */
                conn->flags |= CO_FL_SSL_WAIT_HS | CO_FL_WAIT_L6_CONN;
+#if OPENSSL_VERSION_NUMBER >= 0x0101000L
+               conn->flags |= CO_FL_EARLY_SSL_HS;
+#endif
 
                sslconns++;
                totalsslconns++;
@@ -4654,6 +4682,26 @@ int ssl_sock_handshake(struct connection *conn, unsigned int flag)
        if (!conn->xprt_ctx)
                goto out_error;
 
+#if OPENSSL_VERSION_NUMBER >= 0x10101000L
+       /*
+        * Check if we have early data. If we do, we have to read them
+        * before SSL_do_handshake() is called, And there's no way to
+        * detect early data, except to try to read them
+        */
+       if (conn->flags & CO_FL_EARLY_SSL_HS) {
+               size_t read_data;
+
+               ret = SSL_read_early_data(conn->xprt_ctx, &conn->tmp_early_data,
+                   1, &read_data);
+               if (ret == SSL_READ_EARLY_DATA_ERROR)
+                       goto check_error;
+               if (ret == SSL_READ_EARLY_DATA_SUCCESS) {
+                       conn->flags &= ~(CO_FL_SSL_WAIT_HS | CO_FL_WAIT_L6_CONN);
+                       return 1;
+               } else
+                       conn->flags &= ~CO_FL_EARLY_SSL_HS;
+       }
+#endif
        /* If we use SSL_do_handshake to process a reneg initiated by
         * the remote peer, it sometimes returns SSL_ERROR_SSL.
         * Usually SSL_write and SSL_read are used and process implicitly
@@ -4753,8 +4801,8 @@ int ssl_sock_handshake(struct connection *conn, unsigned int flag)
                /* read some data: consider handshake completed */
                goto reneg_ok;
        }
-
        ret = SSL_do_handshake(conn->xprt_ctx);
+check_error:
        if (ret != 1) {
                /* handshake did not complete, let's find why */
                ret = SSL_get_error(conn->xprt_ctx, ret);
@@ -4844,6 +4892,13 @@ reneg_ok:
         */
        if (global_ssl.async)
                SSL_clear_mode(conn->xprt_ctx, SSL_MODE_ASYNC);
+#endif
+#if OPENSSL_VERSION_NUMBER >= 0x10101000L
+       /* Once the handshake succeeded, we can consider the early data
+        * as valid.
+        */
+       if (conn->flags & CO_FL_EARLY_DATA)
+               conn->flags &= ~CO_FL_EARLY_DATA;
 #endif
        /* Handshake succeeded */
        if (!SSL_session_reused(conn->xprt_ctx)) {
@@ -4913,8 +4968,18 @@ static int ssl_sock_to_buf(struct connection *conn, struct buffer *buf, int coun
                return 0;
 
        /* let's realign the buffer to optimize I/O */
-       if (buffer_empty(buf))
+       if (buffer_empty(buf)) {
                buf->p = buf->data;
+#if (OPENSSL_VERSION_NUMBER >= 0x10101000L)
+               /*
+                * If we're done reading the early data, and we're using
+                * a new buffer, then we know for sure we're not tainted
+                * with early data anymore
+                */
+               if ((conn->flags & (CO_FL_EARLY_SSL_HS |CO_FL_EARLY_DATA)) == CO_FL_EARLY_DATA)
+                       conn->flags &= ~CO_FL_EARLY_DATA;
+#endif
+       }
 
        /* read the largest possible block. For this, we perform only one call
         * to recv() unless the buffer wraps and we exactly fill the first hunk,
@@ -4922,6 +4987,8 @@ static int ssl_sock_to_buf(struct connection *conn, struct buffer *buf, int coun
         * EINTR too.
         */
        while (count > 0) {
+               int need_out = 0;
+
                /* first check if we have some room after p+i */
                try = buf->data + buf->size - (buf->p + buf->i);
                /* otherwise continue between data and p-o */
@@ -4932,7 +4999,42 @@ static int ssl_sock_to_buf(struct connection *conn, struct buffer *buf, int coun
                }
                if (try > count)
                        try = count;
+               if (((conn->flags & (CO_FL_EARLY_SSL_HS | CO_FL_EARLY_DATA)) == CO_FL_EARLY_SSL_HS) &&
+                   conn->tmp_early_data != -1) {
+                       *bi_end(buf) = conn->tmp_early_data;
+                       done++;
+                       try--;
+                       count--;
+                       buf->i++;
+                       conn->tmp_early_data = -1;
+                       continue;
+               }
 
+#if (OPENSSL_VERSION_NUMBER >= 0x10101000L)
+               if (conn->flags & CO_FL_EARLY_SSL_HS) {
+                       size_t read_length;
+
+                       ret = SSL_read_early_data(conn->xprt_ctx,
+                           bi_end(buf), try, &read_length);
+                       if (read_length > 0)
+                               conn->flags |= CO_FL_EARLY_DATA;
+                       if (ret == SSL_READ_EARLY_DATA_SUCCESS ||
+                           ret == SSL_READ_EARLY_DATA_FINISH) {
+                               if (ret == SSL_READ_EARLY_DATA_FINISH) {
+                                       /*
+                                        * We're done reading the early data,
+                                        * let's make the handshake
+                                        */
+                                       conn->flags &= ~CO_FL_EARLY_SSL_HS;
+                                       conn->flags |= CO_FL_SSL_WAIT_HS;
+                                       need_out = 1;
+                                       if (read_length == 0)
+                                               break;
+                               }
+                               ret = read_length;
+                       }
+               } else
+#endif
                ret = SSL_read(conn->xprt_ctx, bi_end(buf), try);
                if (conn->flags & CO_FL_ERROR) {
                        /* CO_FL_ERROR may be set by ssl_sock_infocbk */
@@ -4943,20 +5045,6 @@ static int ssl_sock_to_buf(struct connection *conn, struct buffer *buf, int coun
                        done += ret;
                        count -= ret;
                }
-               else if (ret == 0) {
-                       ret =  SSL_get_error(conn->xprt_ctx, ret);
-                       if (ret != SSL_ERROR_ZERO_RETURN) {
-                               /* error on protocol or underlying transport */
-                               if ((ret != SSL_ERROR_SYSCALL)
-                                    || (errno && (errno != EAGAIN)))
-                                       conn->flags |= CO_FL_ERROR;
-
-                               /* Clear openssl global errors stack */
-                               ssl_sock_dump_errors(conn);
-                               ERR_clear_error();
-                       }
-                       goto read0;
-               }
                else {
                        ret =  SSL_get_error(conn->xprt_ctx, ret);
                        if (ret == SSL_ERROR_WANT_WRITE) {
@@ -4985,10 +5073,13 @@ static int ssl_sock_to_buf(struct connection *conn, struct buffer *buf, int coun
                                /* we need to poll for retry a read later */
                                fd_cant_recv(conn->handle.fd);
                                break;
-                       }
+                       } else if (ret == SSL_ERROR_ZERO_RETURN)
+                               goto read0;
                        /* otherwise it's a real error */
                        goto out_error;
                }
+               if (need_out)
+                       break;
        }
  leave:
        conn_cond_update_sock_polling(conn);
@@ -5036,6 +5127,10 @@ static int ssl_sock_from_buf(struct connection *conn, struct buffer *buf, int fl
         * in which case we accept to do it once again.
         */
        while (buf->o) {
+#if (OPENSSL_VERSION_NUMBER >= 0x10101000L)
+               size_t written_data;
+#endif
+
                try = bo_contig_data(buf);
 
                if (!(flags & CO_SFL_STREAMER) &&
@@ -5051,6 +5146,27 @@ static int ssl_sock_from_buf(struct connection *conn, struct buffer *buf, int fl
                        conn->xprt_st |= SSL_SOCK_SEND_UNLIMITED;
                }
 
+#if (OPENSSL_VERSION_NUMBER >= 0x10101000L)
+               if (!SSL_is_init_finished(conn->xprt_ctx)) {
+                       unsigned int max_early;
+
+                       if (conn->tmp_early_data == -1)
+                               conn->tmp_early_data = 0;
+
+                       max_early = SSL_get_max_early_data(conn->xprt_ctx);
+                       if (try + conn->tmp_early_data > max_early) {
+                               try -= (try + conn->tmp_early_data) - max_early;
+                               if (try <= 0)
+                                       break;
+                       }
+                       ret = SSL_write_early_data(conn->xprt_ctx, bo_ptr(buf), try, &written_data);
+                       if (ret == 1) {
+                               ret = written_data;
+                               conn->tmp_early_data += ret;
+                       }
+
+               } else
+#endif
                ret = SSL_write(conn->xprt_ctx, bo_ptr(buf), try);
 
                if (conn->flags & CO_FL_ERROR) {
@@ -6841,6 +6957,19 @@ static int bind_parse_no_tls_tickets(char **args, int cur_arg, struct proxy *px,
        return 0;
 }
 
+/* parse the "allow-0rtt" bind keyword */
+static int ssl_bind_parse_allow_0rtt(char **args, int cur_arg, struct proxy *px, struct ssl_bind_conf *conf, char **err)
+{
+       conf->early_data = 1;
+       return 0;
+}
+
+static int bind_parse_allow_0rtt(char **args, int cur_arg, struct proxy *px, struct bind_conf *conf, char **err)
+{
+       conf->ssl_options |= BC_SSL_O_EARLY_DATA;
+       return 0;
+}
+
 /* parse the "npn" bind keyword */
 static int ssl_bind_parse_npn(char **args, int cur_arg, struct proxy *px, struct ssl_bind_conf *conf, char **err)
 {
@@ -7380,6 +7509,8 @@ static int ssl_parse_default_bind_options(char **args, int section_type, struct
        while (*(args[i])) {
                if (!strcmp(args[i], "no-tls-tickets"))
                        global_ssl.listen_default_ssloptions |= BC_SSL_O_NO_TLS_TICKETS;
+               else if (!strcmp(args[i], "allow-0rtt"))
+                       global_ssl.listen_default_ssloptions |= BC_SSL_O_EARLY_DATA;
                else if (!strcmp(args[i], "prefer-client-ciphers"))
                        global_ssl.listen_default_ssloptions |= BC_SSL_O_PREF_CLIE_CIPH;
                else if (!strcmp(args[i], "ssl-min-ver") || !strcmp(args[i], "ssl-max-ver")) {
@@ -8045,6 +8176,7 @@ static struct acl_kw_list acl_kws = {ILH, {
  * not enabled.
  */
 static struct ssl_bind_kw ssl_bind_kws[] = {
+       { "allow-0rtt",            ssl_bind_parse_allow_0rtt,       0 }, /* allow 0-RTT */
        { "alpn",                  ssl_bind_parse_alpn,             1 }, /* set ALPN supported protocols */
        { "ca-file",               ssl_bind_parse_ca_file,          1 }, /* set CAfile to process verify on client cert */
        { "ciphers",               ssl_bind_parse_ciphers,          1 }, /* set SSL cipher suite */
@@ -8060,6 +8192,7 @@ static struct ssl_bind_kw ssl_bind_kws[] = {
 };
 
 static struct bind_kw_list bind_kws = { "SSL", { }, {
+       { "allow-0rtt",            bind_parse_allow_0rtt,         0 }, /* Allow 0RTT */
        { "alpn",                  bind_parse_alpn,               1 }, /* set ALPN supported protocols */
        { "ca-file",               bind_parse_ca_file,            1 }, /* set CAfile to process verify on client cert */
        { "ca-ignore-err",         bind_parse_ignore_err,         1 }, /* set error IDs to ignore on verify depth > 0 */