[openssl] master update

dev at ddvo.net dev at ddvo.net
Fri May 14 17:28:38 UTC 2021


The branch master has been updated
       via  647a5dbf10227d65919b49d078da4eaca313f921 (commit)
       via  e2c38c1a4e034f3fac817870902db6d8bc117119 (commit)
       via  be799eb7a3a7d0012dfa27ade1fa68319a40c6c6 (commit)
       via  8b5ca5111ed9d7907e2de91a5af5b5407a46eaf1 (commit)
       via  829902879eb7ba1260a9444f6b6b91d84ca61037 (commit)
       via  22fe2b129922bc9322c41ce8beff1551c078c838 (commit)
       via  8801240bc5d5e7fe29b2635bbf9c4d45fd1b2996 (commit)
       via  19f97fe6f10bf0d1daec26a9ae2ad919127c67d5 (commit)
       via  19a39b29e846e465ee97e7519acf14ddc9302198 (commit)
       via  ca8f823ffd955493b5f7ce85b7511b758f2a982e (commit)
       via  cc1af4dbfe61317e3ade562bd80201f775d01ee6 (commit)
       via  5a0e05413aa54ee9b463e3f59eefeb3aa35d0958 (commit)
       via  35d445be2cc7afc916cead51923754e6858f46f2 (commit)
       via  68bb06f778ccd5c8d48edef5234d11a4158fae77 (commit)
      from  bbf5ccfd8729120e067de709c43be0a4cdfb423b (commit)


- Log -----------------------------------------------------------------
commit 647a5dbf10227d65919b49d078da4eaca313f921
Author: Dr. David von Oheimb <David.von.Oheimb at siemens.com>
Date:   Tue May 11 15:45:22 2021 +0200

    Add OSSL_ prefix to HTTP_DEFAULT_MAX_{LINE_LENGTH,RESP_LEN}
    
    Reviewed-by: Tomas Mraz <tomas at openssl.org>
    (Merged from https://github.com/openssl/openssl/pull/15053)

commit e2c38c1a4e034f3fac817870902db6d8bc117119
Author: Dr. David von Oheimb <David.von.Oheimb at siemens.com>
Date:   Tue May 4 16:58:59 2021 +0200

    http_client.c: Rename internal fields and functions for consistency
    
    Reviewed-by: Tomas Mraz <tomas at openssl.org>
    (Merged from https://github.com/openssl/openssl/pull/15053)

commit be799eb7a3a7d0012dfa27ade1fa68319a40c6c6
Author: Dr. David von Oheimb <David.von.Oheimb at siemens.com>
Date:   Tue May 4 16:33:19 2021 +0200

    HTTP client: Allow streaming of response data (with possibly indefinite length)
    
    Also clean up max_resp_len and add OSSL_HTTP_REQ_CTX_get_resp_len().
    
    Reviewed-by: Tomas Mraz <tomas at openssl.org>
    (Merged from https://github.com/openssl/openssl/pull/15053)

commit 8b5ca5111ed9d7907e2de91a5af5b5407a46eaf1
Author: Dr. David von Oheimb <David.von.Oheimb at siemens.com>
Date:   Tue May 4 11:15:36 2021 +0200

    HTTP client: Allow streaming of request data (for POST method)
    
    Also clean up OSSL_HTTP_REQ_CTX_nbio() states and make it more efficient.
    
    Reviewed-by: Tomas Mraz <tomas at openssl.org>
    (Merged from https://github.com/openssl/openssl/pull/15053)

commit 829902879eb7ba1260a9444f6b6b91d84ca61037
Author: Dr. David von Oheimb <David.von.Oheimb at siemens.com>
Date:   Mon May 3 16:33:10 2021 +0200

    HTTP client API: Generalize to arbitrary request and response contents
    
    Reviewed-by: Tomas Mraz <tomas at openssl.org>
    (Merged from https://github.com/openssl/openssl/pull/15053)

commit 22fe2b129922bc9322c41ce8beff1551c078c838
Author: Dr. David von Oheimb <David.von.Oheimb at siemens.com>
Date:   Sat May 1 22:04:17 2021 +0200

    OSSL_HTTP_transfer(): Fix error reporting in case rctx->server is NULL
    
    Also improve doc of OSSL_parse_url() and OSSL_HTTP_parse_url().
    
    Reviewed-by: Tomas Mraz <tomas at openssl.org>
    (Merged from https://github.com/openssl/openssl/pull/15053)

commit 8801240bc5d5e7fe29b2635bbf9c4d45fd1b2996
Author: Dr. David von Oheimb <David.von.Oheimb at siemens.com>
Date:   Sat May 1 19:47:38 2021 +0200

    OSSL_HTTP_get(): Do not close connection if redirect to same server
    
    Reviewed-by: Tomas Mraz <tomas at openssl.org>
    (Merged from https://github.com/openssl/openssl/pull/15053)

commit 19f97fe6f10bf0d1daec26a9ae2ad919127c67d5
Author: Dr. David von Oheimb <David.von.Oheimb at siemens.com>
Date:   Wed Apr 28 00:26:14 2021 +0200

    HTTP: Implement persistent connections (keep-alive)
    
    Both at API and at CLI level (for the CMP app only, so far)
    there is a new parameter/option: keep_alive.
    * 0 means HTTP connections are not kept open after
    receiving a response, which is the default behavior for HTTP 1.0.
    * 1 means that persistent connections are requested.
    * 2 means that persistent connections are required, i.e.,
    in case the server does not grant them an error occurs.
    
    For the CMP app the default value is 1, which means preferring to keep
    the connection open. For all other internal uses of the HTTP client
    (fetching an OCSP response, a cert, or a CRL) it does not matter
    because these operations just take one round trip.
    
    If the client application requested or required a persistent connection
    and this was granted by the server, it can keep the OSSL_HTTP_REQ_CTX *
    as long as it wants to send further requests and OSSL_HTTP_is_alive()
    returns nonzero,
    else it should call OSSL_HTTP_REQ_CTX_free() or OSSL_HTTP_close().
    In case the client application keeps the OSSL_HTTP_REQ_CTX *
    but the connection then dies for any reason at the server side, it will
    notice this obtaining an I/O error when trying to send the next request.
    
    This requires extending the HTTP header parsing and
    rearranging the high-level HTTP client API. In particular:
    * Split the monolithic OSSL_HTTP_transfer() into OSSL_HTTP_open(),
      OSSL_HTTP_set_request(), a lean OSSL_HTTP_transfer(), and OSSL_HTTP_close().
    * Split the timeout functionality accordingly and improve default behavior.
    * Extract part of OSSL_HTTP_REQ_CTX_new() to OSSL_HTTP_REQ_CTX_set_expected().
    * Extend struct ossl_http_req_ctx_st accordingly.
    
    Use the new feature for the CMP client, which requires extending
    related transaction management of CMP client and test server.
    
    Update the documentation and extend the tests accordingly.
    
    Reviewed-by: Tomas Mraz <tomas at openssl.org>
    (Merged from https://github.com/openssl/openssl/pull/15053)

commit 19a39b29e846e465ee97e7519acf14ddc9302198
Author: Dr. David von Oheimb <David.von.Oheimb at siemens.com>
Date:   Sat May 1 19:26:53 2021 +0200

    OSSL_HTTP_REQ_CTX_add1_headers(): Fix use with host == NULL (relative URLs)
    
    Reviewed-by: Tomas Mraz <tomas at openssl.org>
    (Merged from https://github.com/openssl/openssl/pull/15053)

commit ca8f823ffd955493b5f7ce85b7511b758f2a982e
Author: Dr. David von Oheimb <David.von.Oheimb at siemens.com>
Date:   Mon May 10 14:36:20 2021 +0200

    CMP test server: Extend error reporting on cert rejected for revocation
    
    Reviewed-by: Tomas Mraz <tomas at openssl.org>
    (Merged from https://github.com/openssl/openssl/pull/15053)

commit cc1af4dbfe61317e3ade562bd80201f775d01ee6
Author: Dr. David von Oheimb <David.von.Oheimb at siemens.com>
Date:   Mon May 10 09:37:36 2021 +0200

    HTTP test server: Improve connection management and logging
    
    Reviewed-by: Tomas Mraz <tomas at openssl.org>
    (Merged from https://github.com/openssl/openssl/pull/15053)

commit 5a0e05413aa54ee9b463e3f59eefeb3aa35d0958
Author: Dr. David von Oheimb <David.von.Oheimb at siemens.com>
Date:   Mon May 10 09:32:53 2021 +0200

    cmp_server.c: Improve transaction management and logging
    
    Reviewed-by: Tomas Mraz <tomas at openssl.org>
    (Merged from https://github.com/openssl/openssl/pull/15053)

commit 35d445be2cc7afc916cead51923754e6858f46f2
Author: Dr. David von Oheimb <David.von.Oheimb at siemens.com>
Date:   Mon May 10 14:38:36 2021 +0200

    OSSL_CMP_SRV_process_request(): Log any error queue entries on response
    
    Reviewed-by: Tomas Mraz <tomas at openssl.org>
    (Merged from https://github.com/openssl/openssl/pull/15053)

commit 68bb06f778ccd5c8d48edef5234d11a4158fae77
Author: Dr. David von Oheimb <David.von.Oheimb at siemens.com>
Date:   Wed May 12 08:37:54 2021 +0200

    HTTP client: Rename 'maxline' parameter to 'buf_size' for clarity
    
    Reviewed-by: Tomas Mraz <tomas at openssl.org>
    (Merged from https://github.com/openssl/openssl/pull/15053)

-----------------------------------------------------------------------

Summary of changes:
 NEWS.md                                            |   6 +-
 apps/cmp.c                                         |  73 ++-
 apps/cmp_mock_srv.c                                |   3 +-
 apps/include/apps.h                                |   1 +
 apps/include/http_server.h                         |  23 +-
 apps/lib/apps.c                                    |   7 +-
 apps/lib/http_server.c                             | 130 +++-
 apps/ocsp.c                                        |  14 +-
 crypto/cmp/cmp_ctx.c                               |  17 +-
 crypto/cmp/cmp_http.c                              |  38 +-
 crypto/cmp/cmp_local.h                             |   2 +
 crypto/cmp/cmp_server.c                            |  25 +-
 crypto/err/openssl.txt                             |   2 +
 crypto/http/http_client.c                          | 661 ++++++++++++---------
 crypto/http/http_err.c                             |   4 +
 crypto/ocsp/ocsp_http.c                            |  24 +-
 crypto/x509/x_all.c                                |   2 +-
 doc/man3/OSSL_CMP_CTX_new.pod                      |  13 +-
 doc/man3/OSSL_CMP_SRV_CTX_new.pod                  |   4 +
 doc/man3/OSSL_HTTP_REQ_CTX.pod                     |   6 +-
 doc/man3/OSSL_HTTP_parse_url.pod                   |   8 +-
 doc/man3/OSSL_HTTP_transfer.pod                    |   3 +-
 include/openssl/http.h                             |   4 +-
 test/http_test.c                                   | 187 ++++--
 .../80-test_cmp_http_data/test_connection.csv      |  92 +--
 25 files changed, 887 insertions(+), 462 deletions(-)

diff --git a/NEWS.md b/NEWS.md
index 3193ce6149..78d0772b9a 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -53,8 +53,10 @@ OpenSSL 3.0
     also covering CRMF (RFC 4211) and HTTP transfer (RFC 6712).
     It is part of the crypto lib and adds a 'cmp' app with a demo configuration.
     All widely used CMP features are supported for both clients and servers.
-  * Added a proper HTTP(S) client to libcrypto supporting GET and POST,
-    redirection, plain and ASN.1-encoded contents, proxies, and timeouts.
+  * Added a proper HTTP client supporting GET with optional redirection, POST,
+    arbitrary request and response content types, TLS, persistent connections,
+    connections via HTTP(s) proxies, connections and exchange via user-defined
+    BIOs (allowing implicit connections), and timeout checks.
   * Added util/check-format.pl for checking adherence to the coding guidelines.
   * Added OSSL_ENCODER, a generic encoder API.
   * Added OSSL_PARAM_BLD, an easier to use API to OSSL_PARAM.
diff --git a/apps/cmp.c b/apps/cmp.c
index f64cb8c813..70ca9a34fd 100644
--- a/apps/cmp.c
+++ b/apps/cmp.c
@@ -72,6 +72,7 @@ static char *opt_path = NULL;
 static char *opt_proxy = NULL;
 static char *opt_no_proxy = NULL;
 static char *opt_recipient = NULL;
+static int opt_keep_alive = 1;
 static int opt_msg_timeout = -1;
 static int opt_total_timeout = -1;
 
@@ -205,7 +206,7 @@ typedef enum OPTION_choice {
 
     OPT_SERVER, OPT_PATH, OPT_PROXY, OPT_NO_PROXY,
     OPT_RECIPIENT,
-    OPT_MSG_TIMEOUT, OPT_TOTAL_TIMEOUT,
+    OPT_KEEP_ALIVE, OPT_MSG_TIMEOUT, OPT_TOTAL_TIMEOUT,
 
     OPT_TRUSTED, OPT_UNTRUSTED, OPT_SRVCERT,
     OPT_EXPECT_SENDER,
@@ -344,8 +345,10 @@ const OPTIONS cmp_options[] = {
      "Default from environment variable 'no_proxy', else 'NO_PROXY', else none"},
     {"recipient", OPT_RECIPIENT, 's',
      "DN of CA. Default: subject of -srvcert, -issuer, issuer of -oldcert or -cert"},
+    {"keep_alive", OPT_KEEP_ALIVE, 'N',
+     "Persistent HTTP connections. 0: no, 1 (the default): request, 2: require"},
     {"msg_timeout", OPT_MSG_TIMEOUT, 'N',
-     "Timeout per CMP message round trip (or 0 for none). Default 120 seconds"},
+     "Number of seconds allowed per CMP message round trip, or 0 for infinite"},
     {"total_timeout", OPT_TOTAL_TIMEOUT, 'N',
      "Overall time an enrollment incl. polling may take. Default 0 = infinite"},
 
@@ -530,7 +533,7 @@ static varref cmp_vars[] = { /* must be in same order as enumerated above! */
     {&opt_oldcert}, {(char **)&opt_revreason},
 
     {&opt_server}, {&opt_path}, {&opt_proxy}, {&opt_no_proxy},
-    {&opt_recipient},
+    {&opt_recipient}, {(char **)&opt_keep_alive},
     {(char **)&opt_msg_timeout}, {(char **)&opt_total_timeout},
 
     {&opt_trusted}, {&opt_untrusted}, {&opt_srvcert},
@@ -1817,6 +1820,15 @@ static int setup_client_ctx(OSSL_CMP_CTX *ctx, ENGINE *engine)
     if (!setup_verification_ctx(ctx))
         goto err;
 
+    if (opt_keep_alive != 1)
+        (void)OSSL_CMP_CTX_set_option(ctx, OSSL_CMP_OPT_KEEP_ALIVE,
+                                      opt_keep_alive);
+    if (opt_total_timeout > 0 && opt_msg_timeout > 0
+            && opt_total_timeout < opt_msg_timeout) {
+        CMP_err2("-total_timeout argument = %d must not be < %d (-msg_timeout)",
+                 opt_total_timeout, opt_msg_timeout);
+        goto err;
+    }
     if (opt_msg_timeout >= 0) /* must do this before setup_ssl_ctx() */
         (void)OSSL_CMP_CTX_set_option(ctx, OSSL_CMP_OPT_MSG_TIMEOUT,
                                       opt_msg_timeout);
@@ -2232,6 +2244,13 @@ static int get_opts(int argc, char **argv)
         case OPT_RECIPIENT:
             opt_recipient = opt_str();
             break;
+        case OPT_KEEP_ALIVE:
+            opt_keep_alive = opt_int_arg();
+            if (opt_keep_alive > 2) {
+                CMP_err("-keep_alive argument must be 0, 1, or 2");
+                goto opthelp;
+            }
+            break;
         case OPT_MSG_TIMEOUT:
             opt_msg_timeout = opt_int_arg();
             break;
@@ -2540,6 +2559,7 @@ int cmp_main(int argc, char **argv)
     X509 *newcert = NULL;
     ENGINE *engine = NULL;
     char mock_server[] = "mock server:1";
+    OSSL_CMP_CTX *srv_cmp_ctx = NULL;
     int ret = 0; /* default: failure */
 
     prog = opt_appname(argv[0]);
@@ -2651,22 +2671,26 @@ int cmp_main(int argc, char **argv)
 
         if ((srv_ctx = setup_srv_ctx(engine)) == NULL)
             goto err;
+        srv_cmp_ctx = OSSL_CMP_SRV_CTX_get0_cmp_ctx(srv_ctx);
         OSSL_CMP_CTX_set_transfer_cb_arg(cmp_ctx, srv_ctx);
-        if (!OSSL_CMP_CTX_set_log_cb(OSSL_CMP_SRV_CTX_get0_cmp_ctx(srv_ctx),
-                                     print_to_bio_out)) {
+        if (!OSSL_CMP_CTX_set_log_cb(srv_cmp_ctx, print_to_bio_out)) {
             CMP_err1("cannot set up error reporting and logging for %s", prog);
             goto err;
         }
+        OSSL_CMP_CTX_set_log_verbosity(srv_cmp_ctx, opt_verbosity);
     }
 
 
     if (opt_port != NULL) { /* act as very basic CMP HTTP server */
+        /* TODO for readability, convert this block to separate function */
 #ifdef OPENSSL_NO_SOCK
         BIO_printf(bio_err, "Cannot act as server - sockets not supported\n");
 #else
         BIO *acbio;
         BIO *cbio = NULL;
+        int keep_alive = 0;
         int msgs = 0;
+        int retry = 1;
 
         if ((acbio = http_server_init_bio(prog, opt_port)) == NULL)
             goto err;
@@ -2677,11 +2701,19 @@ int cmp_main(int argc, char **argv)
 
             ret = http_server_get_asn1_req(ASN1_ITEM_rptr(OSSL_CMP_MSG),
                                            (ASN1_VALUE **)&req, &path,
-                                           &cbio, acbio, prog, 0, 0);
-            if (ret == 0)
-                continue;
-            if (ret++ == -1)
-                break; /* fatal error */
+                                           &cbio, acbio, &keep_alive,
+                                           prog, opt_port, 0, 0);
+            if (ret == 0) { /* no request yet */
+                if (retry) {
+                    sleep(1);
+                    retry = 0;
+                    continue;
+                }
+                ret = 0;
+                goto next;
+            }
+            if (ret++ == -1) /* fatal error */
+                break;
 
             ret = 0;
             msgs++;
@@ -2692,7 +2724,7 @@ int cmp_main(int argc, char **argv)
                              path);
                     OPENSSL_free(path);
                     OSSL_CMP_MSG_free(req);
-                    goto cont;
+                    goto next;
                 }
                 OPENSSL_free(path);
                 resp = OSSL_CMP_CTX_server_perform(cmp_ctx, req);
@@ -2702,18 +2734,25 @@ int cmp_main(int argc, char **argv)
                                                   500, "Internal Server Error");
                     break; /* treated as fatal error */
                 }
-                ret = http_server_send_asn1_resp(cbio, "application/pkixcmp",
+                ret = http_server_send_asn1_resp(cbio, keep_alive,
+                                                 "application/pkixcmp",
                                                  ASN1_ITEM_rptr(OSSL_CMP_MSG),
                                                  (const ASN1_VALUE *)resp);
                 OSSL_CMP_MSG_free(resp);
                 if (!ret)
                     break; /* treated as fatal error */
-            } else {
-                (void)http_server_send_status(cbio, 400, "Bad Request");
             }
-        cont:
-            BIO_free_all(cbio);
-            cbio = NULL;
+        next:
+            if (!ret) { /* on transmission error, cancel CMP transaction */
+                (void)OSSL_CMP_CTX_set1_transactionID(srv_cmp_ctx, NULL);
+                (void)OSSL_CMP_CTX_set1_senderNonce(srv_cmp_ctx, NULL);
+            }
+            if (!ret || !keep_alive
+                || OSSL_CMP_CTX_get_status(srv_cmp_ctx) == -1
+                 /* transaction closed by OSSL_CMP_CTX_server_perform() */) {
+                BIO_free_all(cbio);
+                cbio = NULL;
+            }
         }
         BIO_free_all(cbio);
         BIO_free_all(acbio);
diff --git a/apps/cmp_mock_srv.c b/apps/cmp_mock_srv.c
index 856dbefd97..1e6a27210c 100644
--- a/apps/cmp_mock_srv.c
+++ b/apps/cmp_mock_srv.c
@@ -251,7 +251,8 @@ static OSSL_CMP_PKISI *process_rr(OSSL_CMP_SRV_CTX *srv_ctx,
     if (X509_NAME_cmp(issuer, X509_get_issuer_name(ctx->certOut)) != 0
             || ASN1_INTEGER_cmp(serial,
                                 X509_get0_serialNumber(ctx->certOut)) != 0) {
-        ERR_raise(ERR_LIB_CMP, CMP_R_REQUEST_NOT_ACCEPTED);
+        ERR_raise_data(ERR_LIB_CMP, CMP_R_REQUEST_NOT_ACCEPTED,
+                       "wrong certificate to revoke");
         return NULL;
     }
     return OSSL_CMP_PKISI_dup(ctx->statusOut);
diff --git a/apps/include/apps.h b/apps/include/apps.h
index 207ed41bc7..41178a6e22 100644
--- a/apps/include/apps.h
+++ b/apps/include/apps.h
@@ -285,6 +285,7 @@ ASN1_VALUE *app_http_post_asn1(const char *host, const char *port,
                                const STACK_OF(CONF_VALUE) *headers,
                                const char *content_type,
                                ASN1_VALUE *req, const ASN1_ITEM *req_it,
+                               const char *expected_content_type,
                                long timeout, const ASN1_ITEM *rsp_it);
 # endif
 
diff --git a/apps/include/http_server.h b/apps/include/http_server.h
index 1264753899..ed3f597fbd 100644
--- a/apps/include/http_server.h
+++ b/apps/include/http_server.h
@@ -35,12 +35,14 @@
 #  include <signal.h>
 #  define MAXERRLEN 1000 /* limit error text sent to syslog to 1000 bytes */
 # else
+#  undef LOG_DEBUG
 #  undef LOG_INFO
 #  undef LOG_WARNING
 #  undef LOG_ERR
-#  define LOG_INFO      0
-#  define LOG_WARNING   1
-#  define LOG_ERR       2
+#  define LOG_DEBUG     7
+#  define LOG_INFO      6
+#  define LOG_WARNING   4
+#  define LOG_ERR       3
 # endif
 
 /*-
@@ -65,10 +67,12 @@ BIO *http_server_init_bio(const char *prog, const char *port);
  * Accept an ASN.1-formatted HTTP request
  * it: the expected request ASN.1 type
  * preq: pointer to variable where to place the parsed request
- * pcbio: pointer to variable where to place the BIO for sending the response to
  * ppath: pointer to variable where to place the request path, or NULL
+ * pcbio: pointer to variable where to place the BIO for sending the response to
  * acbio: the listening bio (typically as returned by http_server_init_bio())
- * prog: the name of the current app
+ * found_keep_alive: for returning flag if client requests persistent connection
+ * prog: the name of the current app, for diagnostics only
+ * port: the local port listening to, for diagnostics only
  * accept_get: whether to accept GET requests (in addition to POST requests)
  * timeout: connection timeout (in seconds), or 0 for none/infinite
  * returns 0 in case caller should retry, then *preq == *ppath == *pcbio == NULL
@@ -81,19 +85,22 @@ BIO *http_server_init_bio(const char *prog, const char *port);
  */
 int http_server_get_asn1_req(const ASN1_ITEM *it, ASN1_VALUE **preq,
                              char **ppath, BIO **pcbio, BIO *acbio,
-                             const char *prog, int accept_get, int timeout);
+                             int *found_keep_alive,
+                             const char *prog, const char *port,
+                             int accept_get, int timeout);
 
 /*-
  * Send an ASN.1-formatted HTTP response
  * cbio: destination BIO (typically as returned by http_server_get_asn1_req())
  *       note: cbio should not do an encoding that changes the output length
+ * keep_alive: grant persistent connnection
  * content_type: string identifying the type of the response
  * it: the response ASN.1 type
- * valit: the response ASN.1 type
  * resp: the response to send
  * returns 1 on success, 0 on failure
  */
-int http_server_send_asn1_resp(BIO *cbio, const char *content_type,
+int http_server_send_asn1_resp(BIO *cbio, int keep_alive,
+                               const char *content_type,
                                const ASN1_ITEM *it, const ASN1_VALUE *resp);
 
 /*-
diff --git a/apps/lib/apps.c b/apps/lib/apps.c
index dafcf419bf..fa63410359 100644
--- a/apps/lib/apps.c
+++ b/apps/lib/apps.c
@@ -2504,7 +2504,7 @@ ASN1_VALUE *app_http_get_asn1(const char *url, const char *proxy,
     mem = OSSL_HTTP_get(url, proxy, no_proxy, NULL /* bio */, NULL /* rbio */,
                         app_http_tls_cb, &info, 0 /* buf_size */, headers,
                         expected_content_type, 1 /* expect_asn1 */,
-                        HTTP_DEFAULT_MAX_RESP_LEN, timeout);
+                        OSSL_HTTP_DEFAULT_MAX_RESP_LEN, timeout);
     resp = ASN1_item_d2i_bio(it, mem, NULL);
     BIO_free(mem);
 
@@ -2521,6 +2521,7 @@ ASN1_VALUE *app_http_post_asn1(const char *host, const char *port,
                                const STACK_OF(CONF_VALUE) *headers,
                                const char *content_type,
                                ASN1_VALUE *req, const ASN1_ITEM *req_it,
+                               const char *expected_content_type,
                                long timeout, const ASN1_ITEM *rsp_it)
 {
     APP_HTTP_TLS_INFO info;
@@ -2538,8 +2539,8 @@ ASN1_VALUE *app_http_post_asn1(const char *host, const char *port,
                              proxy, no_proxy, NULL /* bio */, NULL /* rbio */,
                              app_http_tls_cb, &info,
                              0 /* buf_size */, headers, content_type, req_mem,
-                             NULL /* expected_ct */, 1 /* expect_asn1 */,
-                             HTTP_DEFAULT_MAX_RESP_LEN, timeout,
+                             expected_content_type, 1 /* expect_asn1 */,
+                             OSSL_HTTP_DEFAULT_MAX_RESP_LEN, timeout,
                              0 /* keep_alive */);
     BIO_free(req_mem);
     res = ASN1_item_d2i_bio(rsp_it, rsp, NULL);
diff --git a/apps/lib/http_server.c b/apps/lib/http_server.c
index 7626ca9aa4..691e5c9056 100644
--- a/apps/lib/http_server.c
+++ b/apps/lib/http_server.c
@@ -30,7 +30,15 @@
 # endif
 #endif
 
+static int verbosity = LOG_INFO;
+
+#define HTTP_PREFIX "HTTP/"
+#define HTTP_VERSION_PATT "1." /* allow 1.x */
+#define HTTP_PREFIX_VERSION HTTP_PREFIX""HTTP_VERSION_PATT
+#define HTTP_1_0 HTTP_PREFIX_VERSION"0" /* "HTTP/1.0" */
+
 #ifdef HTTP_DAEMON
+
 int multi = 0; /* run multiple responder processes */
 int acfd = (int) INVALID_SOCKET;
 
@@ -49,6 +57,9 @@ void log_message(const char *prog, int level, const char *fmt, ...)
 {
     va_list ap;
 
+    if (verbosity < level)
+        return;
+
     va_start(ap, fmt);
 #ifdef HTTP_DAEMON
     if (multi) {
@@ -56,7 +67,7 @@ void log_message(const char *prog, int level, const char *fmt, ...)
 
         if (vsnprintf(buf, sizeof(buf), fmt, ap) > 0)
             syslog(level, "%s", buf);
-        if (level >= LOG_ERR)
+        if (level <= LOG_ERR)
             ERR_print_errors_cb(print_syslog, &level);
     } else
 #endif
@@ -64,6 +75,7 @@ void log_message(const char *prog, int level, const char *fmt, ...)
         BIO_printf(bio_err, "%s: ", prog);
         BIO_vprintf(bio_err, fmt, ap);
         BIO_printf(bio_err, "\n");
+        (void)BIO_flush(bio_err);
     }
     va_end(ap);
 }
@@ -257,28 +269,36 @@ static int urldecode(char *p)
     return (int)(out - save);
 }
 
+/* if *pcbio != NULL, continue given connected session, else accept new */
+/* if found_keep_alive != NULL, return this way connection persistence state */
 int http_server_get_asn1_req(const ASN1_ITEM *it, ASN1_VALUE **preq,
                              char **ppath, BIO **pcbio, BIO *acbio,
-                             const char *prog, int accept_get, int timeout)
+                             int *found_keep_alive,
+                             const char *prog, const char *port,
+                             int accept_get, int timeout)
 {
-    BIO *cbio = NULL, *getbio = NULL, *b64 = NULL;
+    BIO *cbio = *pcbio, *getbio = NULL, *b64 = NULL;
     int len;
     char reqbuf[2048], inbuf[2048];
     char *meth, *url, *end;
     ASN1_VALUE *req;
-    int ret = 1;
+    int ret = 0;
 
     *preq = NULL;
     if (ppath != NULL)
         *ppath = NULL;
-    *pcbio = NULL;
 
-    /* Connection loss before accept() is routine, ignore silently */
-    if (BIO_do_accept(acbio) <= 0)
-        return 0;
+    if (cbio == NULL) {
+        log_message(prog, LOG_DEBUG,
+                    "Awaiting new connection on port %s...", port);
+        if (BIO_do_accept(acbio) <= 0)
+            /* Connection loss before accept() is routine, ignore silently */
+            return ret;
 
-    cbio = BIO_pop(acbio);
-    *pcbio = cbio;
+        *pcbio = cbio = BIO_pop(acbio);
+    } else {
+        log_message(prog, LOG_DEBUG, "Awaiting next request...");
+    }
     if (cbio == NULL) {
         /* Cannot call http_server_send_status(cbio, ...) */
         ret = -1;
@@ -294,23 +314,32 @@ int http_server_get_asn1_req(const ASN1_ITEM *it, ASN1_VALUE **preq,
 
     /* Read the request line. */
     len = BIO_gets(cbio, reqbuf, sizeof(reqbuf));
-    if (len <= 0) {
-        log_message(prog, LOG_INFO,
-                    "Request line read error or empty request");
+    if (len == 0)
+        return ret;
+    ret = 1;
+    if (len < 0) {
+        log_message(prog, LOG_WARNING, "Request line read error");
         (void)http_server_send_status(cbio, 400, "Bad Request");
         goto out;
     }
+    if ((end = strchr(reqbuf, '\r')) != NULL
+            || (end = strchr(reqbuf, '\n')) != NULL)
+        *end = '\0';
+    log_message(prog, LOG_INFO, "Received request, 1st line: %s", reqbuf);
 
     meth = reqbuf;
     url = meth + 3;
     if ((accept_get && strncmp(meth, "GET ", 4) == 0)
             || (url++, strncmp(meth, "POST ", 5) == 0)) {
+        static const char http_version_str[] = " "HTTP_PREFIX_VERSION;
+        static const size_t http_version_str_len = sizeof(http_version_str) - 1;
+
         /* Expecting (GET|POST) {sp} /URL {sp} HTTP/1.x */
         *(url++) = '\0';
         while (*url == ' ')
             url++;
         if (*url != '/') {
-            log_message(prog, LOG_INFO,
+            log_message(prog, LOG_WARNING,
                         "Invalid %s -- URL does not begin with '/': %s",
                         meth, url);
             (void)http_server_send_status(cbio, 400, "Bad Request");
@@ -322,14 +351,17 @@ int http_server_get_asn1_req(const ASN1_ITEM *it, ASN1_VALUE **preq,
         for (end = url; *end != '\0'; end++)
             if (*end == ' ')
                 break;
-        if (strncmp(end, " HTTP/1.", 7) != 0) {
-            log_message(prog, LOG_INFO,
+        if (strncmp(end, http_version_str, http_version_str_len) != 0) {
+            log_message(prog, LOG_WARNING,
                         "Invalid %s -- bad HTTP/version string: %s",
                         meth, end + 1);
             (void)http_server_send_status(cbio, 400, "Bad Request");
             goto out;
         }
         *end = '\0';
+        /* above HTTP 1.0, connection persistence is the default */
+        if (found_keep_alive != NULL)
+            *found_keep_alive = end[http_version_str_len] > '0';
 
         /*-
          * Skip "GET / HTTP..." requests often used by load-balancers.
@@ -343,7 +375,7 @@ int http_server_get_asn1_req(const ASN1_ITEM *it, ASN1_VALUE **preq,
 
         len = urldecode(url);
         if (len < 0) {
-            log_message(prog, LOG_INFO,
+            log_message(prog, LOG_WARNING,
                         "Invalid %s request -- bad URL encoding: %s",
                         meth, url);
             (void)http_server_send_status(cbio, 400, "Bad Request");
@@ -361,8 +393,9 @@ int http_server_get_asn1_req(const ASN1_ITEM *it, ASN1_VALUE **preq,
             getbio = BIO_push(b64, getbio);
         }
     } else {
-        log_message(prog, LOG_INFO,
-                    "HTTP request does not start with GET/POST: %s", reqbuf);
+        log_message(prog, LOG_WARNING,
+                    "HTTP request does not begin with %sPOST: %s",
+                    accept_get ? "GET or " : "", reqbuf);
         /* TODO provide better diagnosis in case client tries TLS */
         (void)http_server_send_status(cbio, 400, "Bad Request");
         goto out;
@@ -377,15 +410,50 @@ int http_server_get_asn1_req(const ASN1_ITEM *it, ASN1_VALUE **preq,
 
     /* Read and skip past the headers. */
     for (;;) {
+        char *key, *value, *line_end = NULL;
+
         len = BIO_gets(cbio, inbuf, sizeof(inbuf));
         if (len <= 0) {
-            log_message(prog, LOG_ERR,
-                        "Error skipping remaining HTTP headers");
+            log_message(prog, LOG_WARNING, "Error reading HTTP header");
             (void)http_server_send_status(cbio, 400, "Bad Request");
             goto out;
         }
-        if ((inbuf[0] == '\r') || (inbuf[0] == '\n'))
+
+        if (inbuf[0] == '\r' || inbuf[0] == '\n')
             break;
+
+        key = inbuf;
+        value = strchr(key, ':');
+        if (value != NULL) {
+            *(value++) = '\0';
+            while (*value == ' ')
+                value++;
+            line_end = strchr(value, '\r');
+            if (line_end == NULL)
+                line_end = strchr(value, '\n');
+            if (line_end != NULL)
+                *line_end = '\0';
+        } else {
+            log_message(prog, LOG_WARNING,
+                        "Error parsing HTTP header: missing ':'");
+            (void)http_server_send_status(cbio, 400, "Bad Request");
+            goto out;
+        }
+        if (value != NULL && line_end != NULL) {
+            /* https://tools.ietf.org/html/rfc7230#section-6.3 Persistence */
+            if (found_keep_alive != NULL && strcasecmp(key, "Connection") == 0) {
+                if (strcasecmp(value, "keep-alive") == 0)
+                    *found_keep_alive = 1;
+                if (strcasecmp(value, "close") == 0)
+                    *found_keep_alive = 0;
+            }
+        } else {
+            log_message(prog, LOG_WARNING,
+                        "Error parsing HTTP header: missing end of line");
+            (void)http_server_send_status(cbio, 400, "Bad Request");
+            goto out;
+        }
+
     }
 
 # ifdef HTTP_DAEMON
@@ -397,7 +465,9 @@ int http_server_get_asn1_req(const ASN1_ITEM *it, ASN1_VALUE **preq,
     /* Try to read and parse request */
     req = ASN1_item_d2i_bio(it, getbio != NULL ? getbio : cbio, NULL);
     if (req == NULL) {
-        log_message(prog, LOG_ERR, "Error parsing request");
+        log_message(prog, LOG_WARNING,
+                    "Error parsing DER-encoded request content");
+        (void)http_server_send_status(cbio, 400, "Bad Request");
     } else if (ppath != NULL && (*ppath = OPENSSL_strdup(url)) == NULL) {
         log_message(prog, LOG_ERR,
                     "Out of memory allocating %zu bytes", strlen(url) + 1);
@@ -429,11 +499,15 @@ int http_server_get_asn1_req(const ASN1_ITEM *it, ASN1_VALUE **preq,
 }
 
 /* assumes that cbio does not do an encoding that changes the output length */
-int http_server_send_asn1_resp(BIO *cbio, const char *content_type,
+int http_server_send_asn1_resp(BIO *cbio, int keep_alive,
+                               const char *content_type,
                                const ASN1_ITEM *it, const ASN1_VALUE *resp)
 {
-    int ret = BIO_printf(cbio, "HTTP/1.0 200 OK\r\nContent-type: %s\r\n"
-                         "Content-Length: %d\r\n\r\n", content_type,
+    int ret = BIO_printf(cbio, HTTP_1_0" 200 OK\r\n%s"
+                         "Content-type: %s\r\n"
+                         "Content-Length: %d\r\n\r\n",
+                         keep_alive ? "Connection: keep-alive\r\n" : "",
+                         content_type,
                          ASN1_item_i2d(resp, NULL, it)) > 0
             && ASN1_item_i2d_bio(it, cbio, resp) > 0;
 
@@ -443,7 +517,9 @@ int http_server_send_asn1_resp(BIO *cbio, const char *content_type,
 
 int http_server_send_status(BIO *cbio, int status, const char *reason)
 {
-    int ret = BIO_printf(cbio, "HTTP/1.0 %d %s\r\n\r\n", status, reason) > 0;
+    int ret = BIO_printf(cbio, HTTP_1_0" %d %s\r\n\r\n",
+                         /* This implicitly cancels keep-alive */
+                         status, reason) > 0;
 
     (void)BIO_flush(cbio);
     return ret;
diff --git a/apps/ocsp.c b/apps/ocsp.c
index 355b4127c8..dd816c4221 100644
--- a/apps/ocsp.c
+++ b/apps/ocsp.c
@@ -76,7 +76,7 @@ static void make_ocsp_response(BIO *err, OCSP_RESPONSE **resp, OCSP_REQUEST *req
 
 static char **lookup_serial(CA_DB *db, ASN1_INTEGER *ser);
 static int do_responder(OCSP_REQUEST **preq, BIO **pcbio, BIO *acbio,
-                        int timeout);
+                        const char *port, int timeout);
 static int send_ocsp_response(BIO *cbio, const OCSP_RESPONSE *resp);
 static char *prog;
 
@@ -631,7 +631,7 @@ redo_accept:
 #endif
 
         req = NULL;
-        res = do_responder(&req, &cbio, acbio, req_timeout);
+        res = do_responder(&req, &cbio, acbio, port, req_timeout);
         if (res == 0)
             goto redo_accept;
 
@@ -1162,12 +1162,13 @@ static char **lookup_serial(CA_DB *db, ASN1_INTEGER *ser)
 }
 
 static int do_responder(OCSP_REQUEST **preq, BIO **pcbio, BIO *acbio,
-                        int timeout)
+                        const char *port, int timeout)
 {
 #ifndef OPENSSL_NO_SOCK
     return http_server_get_asn1_req(ASN1_ITEM_rptr(OCSP_REQUEST),
                                     (ASN1_VALUE **)preq, NULL, pcbio, acbio,
-                                    prog, 1 /* accept_get */, timeout);
+                                    NULL /* found_keep_alive */,
+                                    prog, port, 1 /* accept_get */, timeout);
 #else
     BIO_printf(bio_err,
                "Error getting OCSP request - sockets not supported\n");
@@ -1179,7 +1180,9 @@ static int do_responder(OCSP_REQUEST **preq, BIO **pcbio, BIO *acbio,
 static int send_ocsp_response(BIO *cbio, const OCSP_RESPONSE *resp)
 {
 #ifndef OPENSSL_NO_SOCK
-    return http_server_send_asn1_resp(cbio, "application/ocsp-response",
+    return http_server_send_asn1_resp(cbio,
+                                      0 /* no keep-alive */,
+                                      "application/ocsp-response",
                                       ASN1_ITEM_rptr(OCSP_RESPONSE),
                                       (const ASN1_VALUE *)resp);
 #else
@@ -1211,6 +1214,7 @@ OCSP_RESPONSE *process_responder(OCSP_REQUEST *req,
         app_http_post_asn1(host, port, path, NULL, NULL /* no proxy used */,
                            ctx, headers, "application/ocsp-request",
                            (ASN1_VALUE *)req, ASN1_ITEM_rptr(OCSP_REQUEST),
+                           "application/ocsp-response",
                            req_timeout, ASN1_ITEM_rptr(OCSP_RESPONSE));
 
     if (resp == NULL)
diff --git a/crypto/cmp/cmp_ctx.c b/crypto/cmp/cmp_ctx.c
index 7e7af63b4a..a09432597b 100644
--- a/crypto/cmp/cmp_ctx.c
+++ b/crypto/cmp/cmp_ctx.c
@@ -115,7 +115,8 @@ OSSL_CMP_CTX *OSSL_CMP_CTX_new(OSSL_LIB_CTX *libctx, const char *propq)
     ctx->status = -1;
     ctx->failInfoCode = -1;
 
-    ctx->msg_timeout = 2 * 60;
+    ctx->keep_alive = 1;
+    ctx->msg_timeout = -1;
 
     if ((ctx->untrusted = sk_X509_new_null()) == NULL)
         goto oom;
@@ -149,6 +150,11 @@ int OSSL_CMP_CTX_reinit(OSSL_CMP_CTX *ctx)
         return 0;
     }
 
+    if (ctx->http_ctx != NULL) {
+        (void)OSSL_HTTP_close(ctx->http_ctx, 1);
+        ossl_cmp_debug(ctx, "disconnected from CMP server");
+        ctx->http_ctx = NULL;
+    }
     ctx->status = -1;
     ctx->failInfoCode = -1;
 
@@ -169,6 +175,10 @@ void OSSL_CMP_CTX_free(OSSL_CMP_CTX *ctx)
     if (ctx == NULL)
         return;
 
+    if (ctx->http_ctx != NULL) {
+        (void)OSSL_HTTP_close(ctx->http_ctx, 1);
+        ossl_cmp_debug(ctx, "disconnected from CMP server");
+    }
     OPENSSL_free(ctx->serverPath);
     OPENSSL_free(ctx->server);
     OPENSSL_free(ctx->proxy);
@@ -1041,6 +1051,9 @@ int OSSL_CMP_CTX_set_option(OSSL_CMP_CTX *ctx, int opt, int val)
     case OSSL_CMP_OPT_MAC_ALGNID:
         ctx->pbm_mac = val;
         break;
+    case OSSL_CMP_OPT_KEEP_ALIVE:
+        ctx->keep_alive = val;
+        break;
     case OSSL_CMP_OPT_MSG_TIMEOUT:
         ctx->msg_timeout = val;
         break;
@@ -1105,6 +1118,8 @@ int OSSL_CMP_CTX_get_option(const OSSL_CMP_CTX *ctx, int opt)
         return EVP_MD_type(ctx->pbm_owf);
     case OSSL_CMP_OPT_MAC_ALGNID:
         return ctx->pbm_mac;
+    case OSSL_CMP_OPT_KEEP_ALIVE:
+        return ctx->keep_alive;
     case OSSL_CMP_OPT_MSG_TIMEOUT:
         return ctx->msg_timeout;
     case OSSL_CMP_OPT_TOTAL_TIMEOUT:
diff --git a/crypto/cmp/cmp_http.c b/crypto/cmp/cmp_http.c
index a358622feb..600955efce 100644
--- a/crypto/cmp/cmp_http.c
+++ b/crypto/cmp/cmp_http.c
@@ -28,6 +28,19 @@
 #include <openssl/cmp.h>
 #include <openssl/err.h>
 
+static int keep_alive(int keep_alive, int body_type)
+{
+    if (keep_alive != 0
+        /* Ask for persistent connection only if may need more round trips */
+            && body_type != OSSL_CMP_PKIBODY_IR
+            && body_type != OSSL_CMP_PKIBODY_CR
+            && body_type != OSSL_CMP_PKIBODY_P10CR
+            && body_type != OSSL_CMP_PKIBODY_KUR
+            && body_type != OSSL_CMP_PKIBODY_POLLREQ)
+        keep_alive = 0;
+    return keep_alive;
+}
+
 /*
  * Send the PKIMessage req and on success return the response, else NULL.
  * Any previous error queue entries will likely be removed by ERR_clear_error().
@@ -55,11 +68,12 @@ OSSL_CMP_MSG *OSSL_CMP_MSG_http_perform(OSSL_CMP_CTX *ctx,
 
     if (ctx->serverPort != 0)
         BIO_snprintf(server_port, sizeof(server_port), "%d", ctx->serverPort);
-
     tls_used = OSSL_CMP_CTX_get_http_cb_arg(ctx) != NULL;
-    ossl_cmp_log2(DEBUG, ctx, "connecting to CMP server %s%s",
-                  ctx->server, tls_used ? " using TLS" : "");
-    rsp = OSSL_HTTP_transfer(NULL, ctx->server, server_port,
+    if (ctx->http_ctx == NULL)
+        ossl_cmp_log3(DEBUG, ctx, "connecting to CMP server %s:%s%s",
+                      ctx->server, server_port, tls_used ? " using TLS" : "");
+
+    rsp = OSSL_HTTP_transfer(&ctx->http_ctx, ctx->server, server_port,
                              ctx->serverPath, tls_used,
                              ctx->proxy, ctx->no_proxy,
                              NULL /* bio */, NULL /* rbio */,
@@ -67,12 +81,22 @@ OSSL_CMP_MSG *OSSL_CMP_MSG_http_perform(OSSL_CMP_CTX *ctx,
                              0 /* buf_size */, headers,
                              content_type_pkix, req_mem,
                              content_type_pkix, 1 /* expect_asn1 */,
-                             HTTP_DEFAULT_MAX_RESP_LEN,
-                             ctx->msg_timeout, 0 /* keep_alive */);
+                             OSSL_HTTP_DEFAULT_MAX_RESP_LEN,
+                             ctx->msg_timeout,
+                             keep_alive(ctx->keep_alive, req->body->type));
     BIO_free(req_mem);
     res = (OSSL_CMP_MSG *)ASN1_item_d2i_bio(it, rsp, NULL);
     BIO_free(rsp);
-    ossl_cmp_debug(ctx, "disconnected from CMP server");
+
+    if (ctx->http_ctx == NULL)
+        ossl_cmp_debug(ctx, "disconnected from CMP server");
+    /*
+     * Note that on normal successful end of the transaction the connection
+     * is not closed at this level, but this will be done by the CMP client
+     * application via OSSL_CMP_CTX_free() or OSSL_CMP_CTX_reinit().
+     */
+    if (res != NULL)
+        ossl_cmp_debug(ctx, "finished reading response from CMP server");
  err:
     sk_CONF_VALUE_pop_free(headers, X509V3_conf_free);
     return res;
diff --git a/crypto/cmp/cmp_local.h b/crypto/cmp/cmp_local.h
index b2a3382079..eee609937b 100644
--- a/crypto/cmp/cmp_local.h
+++ b/crypto/cmp/cmp_local.h
@@ -40,11 +40,13 @@ struct ossl_cmp_ctx_st {
     OSSL_CMP_transfer_cb_t transfer_cb; /* default: OSSL_CMP_MSG_http_perform */
     void *transfer_cb_arg; /* allows to store optional argument to cb */
     /* HTTP-based transfer */
+    OSSL_HTTP_REQ_CTX *http_ctx;
     char *serverPath;
     char *server;
     int serverPort;
     char *proxy;
     char *no_proxy;
+    int keep_alive; /* persistent connection: 0=no, 1=prefer, 2=require */
     int msg_timeout; /* max seconds to wait for each CMP message round trip */
     int total_timeout; /* max number of seconds an enrollment may take, incl. */
     /* attempts polling for a response if a 'waiting' PKIStatus is received */
diff --git a/crypto/cmp/cmp_server.c b/crypto/cmp/cmp_server.c
index 4e8fa6e069..73c14841ca 100644
--- a/crypto/cmp/cmp_server.c
+++ b/crypto/cmp/cmp_server.c
@@ -507,6 +507,8 @@ OSSL_CMP_MSG *OSSL_CMP_SRV_process_request(OSSL_CMP_SRV_CTX *srv_ctx,
 #endif
         }
     }
+    ossl_cmp_log1(DEBUG, ctx,
+                  "received %s", ossl_cmp_bodytype_to_string(req_type));
 
     res = ossl_cmp_msg_check_update(ctx, req, unprotected_exception,
                                     srv_ctx->acceptUnprotected);
@@ -579,7 +581,7 @@ OSSL_CMP_MSG *OSSL_CMP_SRV_process_request(OSSL_CMP_SRV_CTX *srv_ctx,
         }
 
         if ((si = OSSL_CMP_STATUSINFO_new(OSSL_CMP_PKISTATUS_rejection,
-                                          fail_info, NULL)) != NULL) {
+                                          fail_info, data)) != NULL) {
             if (err != 0 && (flags & ERR_TXT_STRING) != 0)
                 data = ERR_reason_error_string(err);
             rsp = ossl_cmp_error_new(srv_ctx->ctx, si,
@@ -588,20 +590,28 @@ OSSL_CMP_MSG *OSSL_CMP_SRV_process_request(OSSL_CMP_SRV_CTX *srv_ctx,
             OSSL_CMP_PKISI_free(si);
         }
     }
+    OSSL_CMP_CTX_print_errors(ctx);
     ctx->secretValue = backup_secret;
 
-    /* possibly close the transaction */
     rsp_type =
         rsp != NULL ? ossl_cmp_msg_get_bodytype(rsp) : OSSL_CMP_PKIBODY_ERROR;
+    if (rsp != NULL)
+        ossl_cmp_log1(DEBUG, ctx,
+                      "sending %s", ossl_cmp_bodytype_to_string(rsp_type));
+    else
+        ossl_cmp_log(ERR, ctx, "cannot send proper CMP response");
+
+    /* possibly close the transaction */
+    ctx->status = -2; /* this indicates transaction is open */
     switch (rsp_type) {
     case OSSL_CMP_PKIBODY_IP:
     case OSSL_CMP_PKIBODY_CP:
     case OSSL_CMP_PKIBODY_KUP:
-    case OSSL_CMP_PKIBODY_RP:
         if (OSSL_CMP_CTX_get_option(ctx, OSSL_CMP_OPT_IMPLICIT_CONFIRM) == 0)
             break;
         /* fall through */
 
+    case OSSL_CMP_PKIBODY_RP:
     case OSSL_CMP_PKIBODY_PKICONF:
     case OSSL_CMP_PKIBODY_GENP:
     case OSSL_CMP_PKIBODY_ERROR:
@@ -609,6 +619,7 @@ OSSL_CMP_MSG *OSSL_CMP_SRV_process_request(OSSL_CMP_SRV_CTX *srv_ctx,
         /* prepare for next transaction, ignoring any errors here: */
         (void)OSSL_CMP_CTX_set1_transactionID(ctx, NULL);
         (void)OSSL_CMP_CTX_set1_senderNonce(ctx, NULL);
+        ctx->status = -1; /* transaction closed */
 
     default: /* not closing transaction in other cases */
         break;
@@ -622,19 +633,19 @@ OSSL_CMP_MSG *OSSL_CMP_SRV_process_request(OSSL_CMP_SRV_CTX *srv_ctx,
  * returns received message on success, else NULL and pushes an element on the
  * error stack.
  */
-OSSL_CMP_MSG * OSSL_CMP_CTX_server_perform(OSSL_CMP_CTX *client_ctx,
-                                           const OSSL_CMP_MSG *req)
+OSSL_CMP_MSG *OSSL_CMP_CTX_server_perform(OSSL_CMP_CTX *client_ctx,
+                                          const OSSL_CMP_MSG *req)
 {
     OSSL_CMP_SRV_CTX *srv_ctx = NULL;
 
     if (client_ctx == NULL || req == NULL) {
         ERR_raise(ERR_LIB_CMP, CMP_R_NULL_ARGUMENT);
-        return 0;
+        return NULL;
     }
 
     if ((srv_ctx = OSSL_CMP_CTX_get_transfer_cb_arg(client_ctx)) == NULL) {
         ERR_raise(ERR_LIB_CMP, CMP_R_TRANSFER_ERROR);
-        return 0;
+        return NULL;
     }
 
     return OSSL_CMP_SRV_process_request(srv_ctx, req);
diff --git a/crypto/err/openssl.txt b/crypto/err/openssl.txt
index 9ad6757857..0bbdd886ce 100644
--- a/crypto/err/openssl.txt
+++ b/crypto/err/openssl.txt
@@ -760,6 +760,7 @@ HTTP_R_ERROR_PARSING_URL:101:error parsing url
 HTTP_R_ERROR_RECEIVING:103:error receiving
 HTTP_R_ERROR_SENDING:102:error sending
 HTTP_R_FAILED_READING_DATA:128:failed reading data
+HTTP_R_HEADER_PARSE_ERROR:126:header parse error
 HTTP_R_INCONSISTENT_CONTENT_LENGTH:120:inconsistent content length
 HTTP_R_INVALID_PORT_NUMBER:123:invalid port number
 HTTP_R_INVALID_URL_PATH:125:invalid url path
@@ -774,6 +775,7 @@ HTTP_R_REDIRECTION_FROM_HTTPS_TO_HTTP:112:redirection from https to http
 HTTP_R_REDIRECTION_NOT_ENABLED:116:redirection not enabled
 HTTP_R_RESPONSE_LINE_TOO_LONG:113:response line too long
 HTTP_R_RESPONSE_PARSE_ERROR:104:response parse error
+HTTP_R_SERVER_CANCELED_CONNECTION:127:server canceled connection
 HTTP_R_SOCK_NOT_SUPPORTED:122:sock not supported
 HTTP_R_STATUS_CODE_UNSUPPORTED:114:status code unsupported
 HTTP_R_TLS_NOT_ENABLED:107:tls not enabled
diff --git a/crypto/http/http_client.c b/crypto/http/http_client.c
index 8069b2f645..cd6a51989f 100644
--- a/crypto/http/http_client.c
+++ b/crypto/http/http_client.c
@@ -27,54 +27,69 @@
 
 #define HTTP_PREFIX "HTTP/"
 #define HTTP_VERSION_PATT "1." /* allow 1.x */
-#define HTTP_VERSION_STR_LEN 3
-#define HTTP_LINE1_MINLEN ((int)strlen(HTTP_PREFIX HTTP_VERSION_PATT "x 200\n"))
+#define HTTP_PREFIX_VERSION HTTP_PREFIX""HTTP_VERSION_PATT
+#define HTTP_1_0 HTTP_PREFIX_VERSION"0" /* "HTTP/1.0" */
+#define HTTP_VERSION_PATT_LEN strlen(HTTP_PREFIX_VERSION)
+#define HTTP_VERSION_STR_LEN (HTTP_VERSION_PATT_LEN + 1)
+#define HTTP_LINE1_MINLEN ((int)strlen(HTTP_PREFIX_VERSION "x 200\n"))
 #define HTTP_VERSION_MAX_REDIRECTIONS 50
 
 #define HTTP_STATUS_CODE_OK                200
 #define HTTP_STATUS_CODE_MOVED_PERMANENTLY 301
 #define HTTP_STATUS_CODE_FOUND             302
 
-
 /* Stateful HTTP request code, supporting blocking and non-blocking I/O */
 
 /* Opaque HTTP request status structure */
 
 struct ossl_http_req_ctx_st {
     int state;                  /* Current I/O state */
-    unsigned char *readbuf;     /* Buffer for reading response by line */
-    int readbuflen;             /* Buffer length, equals maxline */
-    BIO *wbio;                  /* BIO to send request to */
-    BIO *rbio;                  /* BIO to read response from */
-    BIO *mem;                   /* Memory BIO response is built into */
-    int method_POST;            /* HTTP method is "POST" (else "GET") */
-    char *expected_ct;          /* expected Content-Type, or NULL */
-    int expect_asn1;            /* response must be ASN.1-encoded */
-    long len_to_send;           /* number of bytes in request still to send */
-    unsigned long resp_len;     /* length of response */
-    size_t max_resp_len;        /* Maximum length of response */
+    unsigned char *buf;         /* Buffer to write request or read response */
+    int buf_size;               /* Buffer size */
+    int free_wbio;              /* wbio allocated internally, free with ctx */
+    BIO *wbio;                  /* BIO to write/send request to */
+    BIO *rbio;                  /* BIO to read/receive response from */
+    OSSL_HTTP_bio_cb_t upd_fn;  /* Optional BIO update callback used for TLS */
+    void *upd_arg;              /* Optional arg for update callback function */
+    int use_ssl;                /* Use HTTPS */
+    char *proxy;                /* Optional proxy name or URI */
+    char *server;               /* Optional server host name */
+    char *port;                 /* Optional server port */
+    BIO *mem;                   /* Memory BIO holding request/response header */
+    BIO *req;                   /* BIO holding the request provided by caller */
+    int method_POST;            /* HTTP method is POST (else GET) */
+    char *expected_ct;          /* Optional expected Content-Type */
+    int expect_asn1;            /* Response must be ASN.1-encoded */
+    unsigned char *pos;         /* Current position sending data */
+    long len_to_send;           /* Number of bytes still to send */
+    size_t resp_len;            /* Length of response */
+    size_t max_resp_len;        /* Maximum length of response, or 0 */
     int keep_alive;             /* Persistent conn. 0=no, 1=prefer, 2=require */
     time_t max_time;            /* Maximum end time of current transfer, or 0 */
     time_t max_total_time;      /* Maximum end time of total transfer, or 0 */
-    char *redirection_url;      /* Location given with HTTP status 301/302 */
+    char *redirection_url;      /* Location obtained from HTTP status 301/302 */
 };
 
 /* HTTP states */
 
-#define OHS_NOREAD          0x1000 /* If set no reading should be performed */
-#define OHS_ERROR           (0 | OHS_NOREAD) /* Error condition */
-#define OHS_FIRSTLINE       1 /* First line being read */
-#define OHS_REDIRECT        0xa /* Looking for redirection location */
-#define OHS_HEADERS         2 /* MIME headers being read */
-#define OHS_ASN1_HEADER     3 /* HTTP initial header (tag+length) being read */
-#define OHS_CONTENT         4 /* HTTP content octets being read */
-#define OHS_WRITE_INIT     (5 | OHS_NOREAD) /* 1st call: ready to start send */
-#define OHS_WRITE          (6 | OHS_NOREAD) /* Request being sent */
-#define OHS_FLUSH          (7 | OHS_NOREAD) /* Request being flushed */
-#define OHS_DONE           (8 | OHS_NOREAD) /* Completed */
-#define OHS_HTTP_HEADER    (9 | OHS_NOREAD) /* Headers set, w/o final \r\n */
-
-OSSL_HTTP_REQ_CTX *OSSL_HTTP_REQ_CTX_new(BIO *wbio, BIO *rbio, int maxline)
+#define OHS_NOREAD         0x1000 /* If set no reading should be performed */
+#define OHS_ERROR          (0 | OHS_NOREAD) /* Error condition */
+#define OHS_ADD_HEADERS    (1 | OHS_NOREAD) /* Adding header lines to request */
+#define OHS_WRITE_INIT     (2 | OHS_NOREAD) /* 1st call: ready to start send */
+#define OHS_WRITE_HDR      (3 | OHS_NOREAD) /* Request header being sent */
+#define OHS_WRITE_REQ      (4 | OHS_NOREAD) /* Request contents being sent */
+#define OHS_FLUSH          (5 | OHS_NOREAD) /* Request being flushed */
+#define OHS_FIRSTLINE       1 /* First line of response being read */
+#define OHS_HEADERS         2 /* MIME headers of response being read */
+#define OHS_REDIRECT        3 /* MIME headers being read, expecting Location */
+#define OHS_ASN1_HEADER     4 /* ASN1 sequence header (tag+length) being read */
+#define OHS_ASN1_CONTENT    5 /* ASN1 content octets being read */
+#define OHS_ASN1_DONE      (6 | OHS_NOREAD) /* ASN1 content read completed */
+#define OHS_STREAM         (7 | OHS_NOREAD) /* HTTP content stream to be read */
+
+/* Low-level HTTP API implementation */
+
+OSSL_HTTP_REQ_CTX *OSSL_HTTP_REQ_CTX_new(BIO *wbio, BIO *rbio, int buf_size)
 {
     OSSL_HTTP_REQ_CTX *rctx;
 
@@ -86,16 +101,15 @@ OSSL_HTTP_REQ_CTX *OSSL_HTTP_REQ_CTX_new(BIO *wbio, BIO *rbio, int maxline)
     if ((rctx = OPENSSL_zalloc(sizeof(*rctx))) == NULL)
         return NULL;
     rctx->state = OHS_ERROR;
-    rctx->readbuflen = maxline > 0 ? maxline : HTTP_DEFAULT_MAX_LINE_LENGTH;
-    rctx->readbuf = OPENSSL_malloc(rctx->readbuflen);
+    rctx->buf_size = buf_size > 0 ? buf_size : OSSL_HTTP_DEFAULT_MAX_LINE_LEN;
+    rctx->buf = OPENSSL_malloc(rctx->buf_size);
     rctx->wbio = wbio;
     rctx->rbio = rbio;
-    if (rctx->readbuf == NULL) {
+    if (rctx->buf == NULL) {
         OPENSSL_free(rctx);
         return NULL;
     }
-    rctx->resp_len = 0;
-    rctx->max_resp_len = HTTP_DEFAULT_MAX_RESP_LEN;
+    rctx->max_resp_len = OSSL_HTTP_DEFAULT_MAX_RESP_LEN;
     /* everything else is 0, e.g. rctx->len_to_send, or NULL, e.g. rctx->mem  */
     return rctx;
 }
@@ -104,8 +118,19 @@ void OSSL_HTTP_REQ_CTX_free(OSSL_HTTP_REQ_CTX *rctx)
 {
     if (rctx == NULL)
         return;
+    /*
+     * Use BIO_free_all() because bio_update_fn may prepend or append to cbio.
+     * This also frees any (e.g., SSL/TLS) BIOs linked with bio and,
+     * like BIO_reset(bio), calls SSL_shutdown() to notify/alert the peer.
+     */
+    if (rctx->free_wbio)
+        BIO_free_all(rctx->wbio);
+    /* do not free rctx->rbio */
     BIO_free(rctx->mem); /* this may indirectly call ERR_clear_error() */
-    OPENSSL_free(rctx->readbuf);
+    OPENSSL_free(rctx->buf);
+    OPENSSL_free(rctx->proxy);
+    OPENSSL_free(rctx->server);
+    OPENSSL_free(rctx->port);
     OPENSSL_free(rctx->expected_ct);
     OPENSSL_free(rctx);
 }
@@ -135,7 +160,7 @@ void OSSL_HTTP_REQ_CTX_set_max_response_length(OSSL_HTTP_REQ_CTX *rctx,
         ERR_raise(ERR_LIB_HTTP, ERR_R_PASSED_NULL_PARAMETER);
         return;
     }
-    rctx->max_resp_len = len != 0 ? (size_t)len : HTTP_DEFAULT_MAX_RESP_LEN;
+    rctx->max_resp_len = len != 0 ? (size_t)len : OSSL_HTTP_DEFAULT_MAX_RESP_LEN;
 }
 
 /*
@@ -175,9 +200,10 @@ int OSSL_HTTP_REQ_CTX_set_request_line(OSSL_HTTP_REQ_CTX *rctx, int method_POST,
     if (path[0] != '/' && BIO_printf(rctx->mem, "/") <= 0)
         return 0;
 
-    if (BIO_printf(rctx->mem, "%s "HTTP_PREFIX"1.0\r\n", path) <= 0)
+    if (BIO_printf(rctx->mem, "%s "HTTP_1_0"\r\n", path) <= 0)
         return 0;
-    rctx->state = OHS_HTTP_HEADER;
+    rctx->resp_len = 0;
+    rctx->state = OHS_ADD_HEADERS;
     return 1;
 }
 
@@ -201,10 +227,7 @@ int OSSL_HTTP_REQ_CTX_add1_header(OSSL_HTTP_REQ_CTX *rctx,
         if (BIO_puts(rctx->mem, value) <= 0)
             return 0;
     }
-    if (BIO_write(rctx->mem, "\r\n", 2) != 2)
-        return 0;
-    rctx->state = OHS_HTTP_HEADER;
-    return 1;
+    return BIO_write(rctx->mem, "\r\n", 2) == 2;
 }
 
 int OSSL_HTTP_REQ_CTX_set_expected(OSSL_HTTP_REQ_CTX *rctx,
@@ -216,7 +239,7 @@ int OSSL_HTTP_REQ_CTX_set_expected(OSSL_HTTP_REQ_CTX *rctx,
         return 0;
     }
     if (keep_alive != 0
-            && rctx->state != OHS_ERROR && rctx->state != OHS_HEADERS) {
+            && rctx->state != OHS_ERROR && rctx->state != OHS_ADD_HEADERS) {
         /* Cannot anymore set keep-alive in request header */
         ERR_raise(ERR_LIB_HTTP, ERR_R_SHOULD_NOT_HAVE_BEEN_CALLED);
         return 0;
@@ -237,17 +260,23 @@ int OSSL_HTTP_REQ_CTX_set_expected(OSSL_HTTP_REQ_CTX *rctx,
     return 1;
 }
 
-static int ossl_http_req_ctx_set_content(OSSL_HTTP_REQ_CTX *rctx,
-                                         const char *content_type, BIO *req_mem)
+static int set_content(OSSL_HTTP_REQ_CTX *rctx,
+                       const char *content_type, BIO *req)
 {
-    const unsigned char *req;
     long req_len;
 
-    if (rctx == NULL || req_mem == NULL) {
+    if (rctx == NULL || (req == NULL && content_type != NULL)) {
         ERR_raise(ERR_LIB_HTTP, ERR_R_PASSED_NULL_PARAMETER);
         return 0;
     }
-    if (rctx->mem == NULL || !rctx->method_POST) {
+
+    if (rctx->keep_alive != 0
+            && !OSSL_HTTP_REQ_CTX_add1_header(rctx, "Connection", "keep-alive"))
+        return 0;
+
+    if (req == NULL)
+        return 1;
+    if (!rctx->method_POST) {
         ERR_raise(ERR_LIB_HTTP, ERR_R_SHOULD_NOT_HAVE_BEEN_CALLED);
         return 0;
     }
@@ -256,12 +285,10 @@ static int ossl_http_req_ctx_set_content(OSSL_HTTP_REQ_CTX *rctx,
             && BIO_printf(rctx->mem, "Content-Type: %s\r\n", content_type) <= 0)
         return 0;
 
-    if ((req_len = BIO_get_mem_data(req_mem, &req)) <= 0)
-        return 0;
-    rctx->state = OHS_WRITE_INIT;
-
-    return BIO_printf(rctx->mem, "Content-Length: %ld\r\n\r\n", req_len) > 0
-        && BIO_write(rctx->mem, req, req_len) == (int)req_len;
+    rctx->req = req;
+    if ((req_len = BIO_ctrl(req, BIO_CTRL_INFO, 0, NULL)) <= 0)
+        return 1; /* streaming BIO may not support querying size */
+    return BIO_printf(rctx->mem, "Content-Length: %ld\r\n", req_len) > 0;
 }
 
 int OSSL_HTTP_REQ_CTX_set1_req(OSSL_HTTP_REQ_CTX *rctx, const char *content_type,
@@ -276,17 +303,16 @@ int OSSL_HTTP_REQ_CTX_set1_req(OSSL_HTTP_REQ_CTX *rctx, const char *content_type
     }
 
     res = (mem = ASN1_item_i2d_mem_bio(it, req)) != NULL
-        && ossl_http_req_ctx_set_content(rctx, content_type, mem);
+        && set_content(rctx, content_type, mem);
     BIO_free(mem);
     return res;
 }
 
-static int OSSL_HTTP_REQ_CTX_add1_headers(OSSL_HTTP_REQ_CTX *rctx,
-                                          const STACK_OF(CONF_VALUE) *headers,
-                                          const char *host)
+static int add1_headers(OSSL_HTTP_REQ_CTX *rctx,
+                        const STACK_OF(CONF_VALUE) *headers, const char *host)
 {
     int i;
-    int add_host = 1;
+    int add_host = host != NULL && *host != '\0';
     CONF_VALUE *hdr;
 
     for (i = 0; i < sk_CONF_VALUE_num(headers); i++) {
@@ -302,41 +328,36 @@ static int OSSL_HTTP_REQ_CTX_add1_headers(OSSL_HTTP_REQ_CTX *rctx,
     return 1;
 }
 
-/*-
- * Create OSSL_HTTP_REQ_CTX structure using the values provided.
- * If !use_http_proxy then the 'server' and 'port' parameters are ignored.
- * If req_mem == NULL then use GET and ignore content_type, else POST.
- */
-static OSSL_HTTP_REQ_CTX
-*ossl_http_req_ctx_new(BIO *wbio, BIO *rbio, int use_http_proxy,
-                       const char *server, const char *port,
-                       const char *path,
-                       const STACK_OF(CONF_VALUE) *headers,
-                       const char *content_type, BIO *req_mem,
-                       int maxline, int timeout,
-                       const char *expected_ct, int expect_asn1)
+/* Create OSSL_HTTP_REQ_CTX structure using the values provided. */
+static OSSL_HTTP_REQ_CTX *http_req_ctx_new(int free_wbio, BIO *wbio, BIO *rbio,
+                                           OSSL_HTTP_bio_cb_t bio_update_fn,
+                                           void *arg, int use_ssl,
+                                           const char *proxy,
+                                           const char *server, const char *port,
+                                           int buf_size, int overall_timeout)
 {
-    OSSL_HTTP_REQ_CTX *rctx;
-
-    if (use_http_proxy && (server == NULL || port == NULL)) {
-        ERR_raise(ERR_LIB_HTTP, ERR_R_PASSED_NULL_PARAMETER);
-        return NULL;
-    }
-    /* remaining parameters are checked indirectly by the functions called */
+    OSSL_HTTP_REQ_CTX *rctx = OSSL_HTTP_REQ_CTX_new(wbio, rbio, buf_size);
 
-    if ((rctx = OSSL_HTTP_REQ_CTX_new(wbio, rbio, maxline))
-        == NULL)
+    if (rctx == NULL)
         return NULL;
-    if (OSSL_HTTP_REQ_CTX_set_request_line(rctx, req_mem != NULL,
-                                           use_http_proxy ? server : NULL, port,
-                                           path)
-        && OSSL_HTTP_REQ_CTX_set_expected(rctx, expected_ct, expect_asn1,
-                                          timeout, 0)
-        && OSSL_HTTP_REQ_CTX_add1_headers(rctx, headers, server)
-        && (req_mem == NULL
-            || ossl_http_req_ctx_set_content(rctx, content_type, req_mem)))
-        return rctx;
+    rctx->free_wbio = free_wbio;
+    rctx->upd_fn = bio_update_fn;
+    rctx->upd_arg = arg;
+    rctx->use_ssl = use_ssl;
+    if (proxy != NULL
+            && (rctx->proxy = OPENSSL_strdup(proxy)) == NULL)
+        goto err;
+    if (server != NULL
+            && (rctx->server = OPENSSL_strdup(server)) == NULL)
+        goto err;
+    if (port != NULL
+            && (rctx->port = OPENSSL_strdup(port)) == NULL)
+        goto err;
+    rctx->max_total_time =
+        overall_timeout > 0 ? time(NULL) + overall_timeout : 0;
+    return rctx;
 
+ err:
     OSSL_HTTP_REQ_CTX_free(rctx);
     return NULL;
 }
@@ -346,45 +367,42 @@ static OSSL_HTTP_REQ_CTX
  * We need to obtain the numeric code and (optional) informational message.
  */
 
-static int parse_http_line1(char *line)
+static int parse_http_line1(char *line, int *found_keep_alive)
 {
-    int retcode;
+    int i, retcode;
     char *code, *reason, *end;
 
+    if (strncmp(line, HTTP_PREFIX_VERSION, HTTP_VERSION_PATT_LEN) != 0)
+        goto err;
+    /* above HTTP 1.0, connection persistence is the default */
+    *found_keep_alive = line[HTTP_VERSION_PATT_LEN] > '0';
+
     /* Skip to first whitespace (past protocol info) */
     for (code = line; *code != '\0' && !ossl_isspace(*code); code++)
         continue;
-    if (*code == '\0') {
-        ERR_raise(ERR_LIB_HTTP, HTTP_R_RESPONSE_PARSE_ERROR);
-        return 0;
-    }
+    if (*code == '\0')
+        goto err;
 
     /* Skip past whitespace to start of response code */
     while (*code != '\0' && ossl_isspace(*code))
         code++;
-
-    if (*code == '\0') {
-        ERR_raise(ERR_LIB_HTTP, HTTP_R_RESPONSE_PARSE_ERROR);
-        return 0;
-    }
+    if (*code == '\0')
+        goto err;
 
     /* Find end of response code: first whitespace after start of code */
     for (reason = code; *reason != '\0' && !ossl_isspace(*reason); reason++)
         continue;
 
-    if (*reason == '\0') {
-        ERR_raise(ERR_LIB_HTTP, HTTP_R_RESPONSE_PARSE_ERROR);
-        return 0;
-    }
+    if (*reason == '\0')
+        goto err;
 
     /* Set end of response code and start of message */
     *reason++ = '\0';
 
     /* Attempt to parse numeric code */
     retcode = strtoul(code, &end, 10);
-
     if (*end != '\0')
-        return 0;
+        goto err;
 
     /* Skip over any leading whitespace in message */
     while (*reason != '\0' && ossl_isspace(*reason))
@@ -418,16 +436,24 @@ static int parse_http_line1(char *line)
                            "Code=%s, Reason=%s", code, reason);
         return 0;
     }
+
+ err:
+    i = 0;
+    while (i < 60 && ossl_isprint(line[i]))
+        i++;
+    line[i] = '\0';
+    ERR_raise_data(ERR_LIB_HTTP, HTTP_R_HEADER_PARSE_ERROR, "content=%s", line);
+    return 0;
 }
 
-static int check_set_resp_len(OSSL_HTTP_REQ_CTX *rctx, unsigned long len)
+static int check_set_resp_len(OSSL_HTTP_REQ_CTX *rctx, size_t len)
 {
-    if (len > rctx->max_resp_len)
+    if (rctx->max_resp_len != 0 && len > rctx->max_resp_len)
         ERR_raise_data(ERR_LIB_HTTP, HTTP_R_MAX_RESP_LEN_EXCEEDED,
-                       "length=%lu, max=%lu", len, rctx->max_resp_len);
+                       "length=%zu, max=%zu", len, rctx->max_resp_len);
     if (rctx->resp_len != 0 && rctx->resp_len != len)
         ERR_raise_data(ERR_LIB_HTTP, HTTP_R_INCONSISTENT_CONTENT_LENGTH,
-                       "ASN.1 length=%lu, Content-Length=%lu",
+                       "ASN.1 length=%zu, Content-Length=%zu",
                        len, rctx->resp_len);
     rctx->resp_len = len;
     return 1;
@@ -439,9 +465,9 @@ static int check_set_resp_len(OSSL_HTTP_REQ_CTX *rctx, unsigned long len)
  */
 int OSSL_HTTP_REQ_CTX_nbio(OSSL_HTTP_REQ_CTX *rctx)
 {
-    int i;
+    int i, found_expected_ct = 0, found_keep_alive = 0;
     long n;
-    unsigned long resp_len;
+    size_t resp_len;
     const unsigned char *p;
     char *key, *value, *line_end = NULL;
 
@@ -457,7 +483,10 @@ int OSSL_HTTP_REQ_CTX_nbio(OSSL_HTTP_REQ_CTX *rctx)
     rctx->redirection_url = NULL;
  next_io:
     if ((rctx->state & OHS_NOREAD) == 0) {
-        n = BIO_read(rctx->rbio, rctx->readbuf, rctx->readbuflen);
+        if (rctx->expect_asn1)
+            n = BIO_read(rctx->rbio, rctx->buf, rctx->buf_size);
+        else
+            n = BIO_gets(rctx->rbio, (char *)rctx->buf, rctx->buf_size);
         if (n <= 0) {
             if (BIO_should_retry(rctx->rbio))
                 return -1;
@@ -466,12 +495,12 @@ int OSSL_HTTP_REQ_CTX_nbio(OSSL_HTTP_REQ_CTX *rctx)
         }
 
         /* Write data to memory BIO */
-        if (BIO_write(rctx->mem, rctx->readbuf, n) != n)
+        if (BIO_write(rctx->mem, rctx->buf, n) != n)
             return 0;
     }
 
     switch (rctx->state) {
-    case OHS_HTTP_HEADER:
+    case OHS_ADD_HEADERS:
         /* Last operation was adding headers: need a final \r\n */
         if (BIO_write(rctx->mem, "\r\n", 2) != 2) {
             rctx->state = OHS_ERROR;
@@ -481,30 +510,45 @@ int OSSL_HTTP_REQ_CTX_nbio(OSSL_HTTP_REQ_CTX *rctx)
 
         /* fall thru */
     case OHS_WRITE_INIT:
-        rctx->len_to_send = BIO_get_mem_data(rctx->mem, NULL);
-        rctx->state = OHS_WRITE;
+        rctx->len_to_send = BIO_get_mem_data(rctx->mem, &rctx->pos);
+        rctx->state = OHS_WRITE_HDR;
 
         /* fall thru */
-    case OHS_WRITE:
-        n = BIO_get_mem_data(rctx->mem, &p) - rctx->len_to_send;
-        i = BIO_write(rctx->wbio, p + n, rctx->len_to_send);
-
-        if (i <= 0) {
-            if (BIO_should_retry(rctx->wbio))
-                return -1;
-            rctx->state = OHS_ERROR;
-            return 0;
+    case OHS_WRITE_HDR:
+        /* Copy some chunk of data from rctx->mem to rctx->wbio */
+    case OHS_WRITE_REQ:
+        /* Copy some chunk of data from rctx->req to rctx->wbio */
+
+        if (rctx->len_to_send > 0) {
+            i = BIO_write(rctx->wbio, rctx->pos, rctx->len_to_send);
+            if (i <= 0) {
+                if (BIO_should_retry(rctx->wbio))
+                    return -1;
+                rctx->state = OHS_ERROR;
+                return 0;
+            }
+            rctx->pos += i;
+            rctx->len_to_send -= i;
+            goto next_io;
         }
-
-        rctx->len_to_send -= i;
-
-        if (rctx->len_to_send > 0)
+        if (rctx->state == OHS_WRITE_HDR) {
+            (void)BIO_reset(rctx->mem);
+            rctx->state = OHS_WRITE_REQ;
+        }
+        if (rctx->req != NULL && !BIO_eof(rctx->req)) {
+            n = BIO_read(rctx->req, rctx->buf, rctx->buf_size);
+            if (n <= 0) {
+                if (BIO_should_retry(rctx->rbio))
+                    return -1;
+                ERR_raise(ERR_LIB_HTTP, HTTP_R_FAILED_READING_DATA);
+                return 0;
+            }
+            rctx->pos = rctx->buf;
+            rctx->len_to_send = n;
             goto next_io;
-
+        }
         rctx->state = OHS_FLUSH;
 
-        (void)BIO_reset(rctx->mem);
-
         /* fall thru */
     case OHS_FLUSH:
 
@@ -537,13 +581,13 @@ int OSSL_HTTP_REQ_CTX_nbio(OSSL_HTTP_REQ_CTX *rctx)
          */
         n = BIO_get_mem_data(rctx->mem, &p);
         if (n <= 0 || memchr(p, '\n', n) == 0) {
-            if (n >= rctx->readbuflen) {
+            if (n >= rctx->buf_size) {
                 rctx->state = OHS_ERROR;
                 return 0;
             }
             goto next_io;
         }
-        n = BIO_gets(rctx->mem, (char *)rctx->readbuf, rctx->readbuflen);
+        n = BIO_gets(rctx->mem, (char *)rctx->buf, rctx->buf_size);
 
         if (n <= 0) {
             if (BIO_should_retry(rctx->mem))
@@ -553,7 +597,7 @@ int OSSL_HTTP_REQ_CTX_nbio(OSSL_HTTP_REQ_CTX *rctx)
         }
 
         /* Don't allow excessive lines */
-        if (n == rctx->readbuflen) {
+        if (n == rctx->buf_size) {
             ERR_raise(ERR_LIB_HTTP, HTTP_R_RESPONSE_LINE_TOO_LONG);
             rctx->state = OHS_ERROR;
             return 0;
@@ -561,7 +605,7 @@ int OSSL_HTTP_REQ_CTX_nbio(OSSL_HTTP_REQ_CTX *rctx)
 
         /* First line */
         if (rctx->state == OHS_FIRSTLINE) {
-            switch (parse_http_line1((char *)rctx->readbuf)) {
+            switch (parse_http_line1((char *)rctx->buf, &found_keep_alive)) {
             case HTTP_STATUS_CODE_OK:
                 rctx->state = OHS_HEADERS;
                 goto next_line;
@@ -579,7 +623,7 @@ int OSSL_HTTP_REQ_CTX_nbio(OSSL_HTTP_REQ_CTX *rctx)
                 return 0;
             }
         }
-        key = (char *)rctx->readbuf;
+        key = (char *)rctx->buf;
         value = strchr(key, ':');
         if (value != NULL) {
             *(value++) = '\0';
@@ -605,11 +649,17 @@ int OSSL_HTTP_REQ_CTX_nbio(OSSL_HTTP_REQ_CTX *rctx)
                                    rctx->expected_ct, value);
                     return 0;
                 }
-                OPENSSL_free(rctx->expected_ct);
-                rctx->expected_ct = NULL; /* content-type has been found */
+                found_expected_ct = 1;
             }
-            if (strcasecmp(key, "Content-Length") == 0) {
-                resp_len = strtoul(value, &line_end, 10);
+
+            /* https://tools.ietf.org/html/rfc7230#section-6.3 Persistence */
+            if (strcasecmp(key, "Connection") == 0) {
+                if (strcasecmp(value, "keep-alive") == 0)
+                    found_keep_alive = 1;
+                else if (strcasecmp(value, "close") == 0)
+                    found_keep_alive = 0;
+            } else if (strcasecmp(key, "Content-Length") == 0) {
+                resp_len = (size_t)strtoul(value, &line_end, 10);
                 if (line_end == value || *line_end != '\0') {
                     ERR_raise_data(ERR_LIB_HTTP,
                                    HTTP_R_ERROR_PARSING_CONTENT_LENGTH,
@@ -622,18 +672,28 @@ int OSSL_HTTP_REQ_CTX_nbio(OSSL_HTTP_REQ_CTX *rctx)
         }
 
         /* Look for blank line indicating end of headers */
-        for (p = rctx->readbuf; *p != '\0'; p++) {
+        for (p = rctx->buf; *p != '\0'; p++) {
             if (*p != '\r' && *p != '\n')
                 break;
         }
         if (*p != '\0') /* not end of headers */
             goto next_line;
 
-        if (rctx->expected_ct != NULL) {
+        if (rctx->expected_ct != NULL && !found_expected_ct) {
             ERR_raise_data(ERR_LIB_HTTP, HTTP_R_MISSING_CONTENT_TYPE,
                            "expected=%s", rctx->expected_ct);
             return 0;
         }
+        if (rctx->keep_alive != 0 /* do not let server initiate keep_alive */
+                && !found_keep_alive /* otherwise there is no change */) {
+            if (rctx->keep_alive == 2) {
+                rctx->keep_alive = 0;
+                ERR_raise(ERR_LIB_HTTP, HTTP_R_SERVER_CANCELED_CONNECTION);
+                return 0;
+            }
+            rctx->keep_alive = 0;
+        }
+
         if (rctx->state == OHS_REDIRECT) {
             /* http status code indicated redirect but there was no Location */
             ERR_raise(ERR_LIB_HTTP, HTTP_R_MISSING_REDIRECT_LOCATION);
@@ -641,8 +701,8 @@ int OSSL_HTTP_REQ_CTX_nbio(OSSL_HTTP_REQ_CTX *rctx)
         }
 
         if (!rctx->expect_asn1) {
-            rctx->state = OHS_CONTENT;
-            goto content;
+            rctx->state = OHS_STREAM;
+            return 1;
         }
 
         rctx->state = OHS_ASN1_HEADER;
@@ -691,17 +751,16 @@ int OSSL_HTTP_REQ_CTX_nbio(OSSL_HTTP_REQ_CTX *rctx)
         if (!check_set_resp_len(rctx, resp_len))
             return 0;
 
- content:
-        rctx->state = OHS_CONTENT;
+        rctx->state = OHS_ASN1_CONTENT;
 
         /* Fall thru */
-    case OHS_CONTENT:
+    case OHS_ASN1_CONTENT:
     default:
         n = BIO_get_mem_data(rctx->mem, NULL);
-        if (n < (long)rctx->resp_len /* may be 0 if no Content-Type or ASN.1 */)
+        if (n < 0 || (size_t)n < rctx->resp_len)
             goto next_io;
 
-        rctx->state = OHS_DONE;
+        rctx->state = OHS_ASN1_DONE;
         return 1;
     }
 }
@@ -723,7 +782,7 @@ int OSSL_HTTP_REQ_CTX_nbio_d2i(OSSL_HTTP_REQ_CTX *rctx,
 #ifndef OPENSSL_NO_SOCK
 
 /* set up a new connection BIO, to HTTP server or to HTTP(S) proxy if given */
-static BIO *HTTP_new_bio(const char *server /* optionally includes ":port" */,
+static BIO *http_new_bio(const char *server /* optionally includes ":port" */,
                          const char *server_port /* explicit server port */,
                          int use_ssl,
                          const char *proxy /* optionally includes ":port" */,
@@ -755,11 +814,7 @@ static BIO *HTTP_new_bio(const char *server /* optionally includes ":port" */,
 }
 #endif /* OPENSSL_NO_SOCK */
 
-int OSSL_HTTP_is_alive(const OSSL_HTTP_REQ_CTX *rctx)
-{
-    return rctx != NULL && rctx->keep_alive != 0;
-}
-
+/* Exchange request and response via HTTP on (non-)blocking BIO */
 BIO *OSSL_HTTP_REQ_CTX_exchange(OSSL_HTTP_REQ_CTX *rctx)
 {
     int rv;
@@ -788,87 +843,25 @@ BIO *OSSL_HTTP_REQ_CTX_exchange(OSSL_HTTP_REQ_CTX *rctx)
         }
         return NULL;
     }
-    return rctx->mem;
+    return rctx->state == OHS_STREAM ? rctx->rbio : rctx->mem;
 }
 
-static int update_timeout(int timeout, time_t start_time)
+int OSSL_HTTP_is_alive(const OSSL_HTTP_REQ_CTX *rctx)
 {
-    long elapsed_time;
-
-    if (timeout == 0)
-        return 0;
-    elapsed_time = (long)(time(NULL) - start_time); /* this might overflow */
-    return timeout <= elapsed_time ? -1 : timeout - elapsed_time;
+    return rctx != NULL && rctx->keep_alive != 0;
 }
 
+/* High-level HTTP API implementation */
+
+/* Initiate an HTTP session using bio, else use given server, proxy, etc. */
 OSSL_HTTP_REQ_CTX *OSSL_HTTP_open(const char *server, const char *port,
                                   const char *proxy, const char *no_proxy,
                                   int use_ssl, BIO *bio, BIO *rbio,
                                   OSSL_HTTP_bio_cb_t bio_update_fn, void *arg,
                                   int buf_size, int overall_timeout)
 {
-    return NULL; /* TODO(3.0) expand */
-}
-
-/*-
- * Exchange HTTP request and response with the given server.
- * If req_mem == NULL then use GET and ignore content_type, else POST.
- * The redirection_url output (freed by caller) parameter is used only for GET.
- *
- * Typically the bio and rbio parameters are NULL and a network BIO is created
- * internally for connecting to the given server and port, optionally via a
- * proxy and its port, and is then used for exchanging the request and response.
- * If bio is given and rbio is NULL then this BIO is used instead.
- * If both bio and rbio are given (which may be memory BIOs for instance)
- * then no explicit connection is attempted,
- * bio is used for writing the request, and rbio for reading the response.
- *
- * bio_update_fn is an optional BIO connect/disconnect callback function,
- * which has the prototype
- *   BIO *(*OSSL_HTTP_bio_cb_t) (BIO *bio, void *arg, int conn, int detail);
- * The callback may modify the HTTP BIO provided in the bio argument,
- * whereby it may make use of any custom defined argument 'arg'.
- * During connection establishment, just after BIO_do_connect_retry(),
- * the callback function is invoked with the 'conn' argument being 1
- * 'detail' indicating whether a HTTPS (i.e., TLS) connection is requested.
- * On disconnect 'conn' is 0 and 'detail' indicates that no error occurred.
- * For instance, on connect the funct may prepend a TLS BIO to implement HTTPS;
- * after disconnect it may do some error diagnostics and/or specific cleanup.
- * The function should return NULL to indicate failure.
- * After disconnect the modified BIO will be deallocated using BIO_free_all().
- */
-int OSSL_HTTP_set_request(OSSL_HTTP_REQ_CTX *rctx, const char *path,
-                          const STACK_OF(CONF_VALUE) *headers,
-                          const char *content_type, BIO *req,
-                          const char *expected_content_type, int expect_asn1,
-                          size_t max_resp_len, int timeout, int keep_alive)
-{
-    return 0; /* TODO(3.0) expand */
-}
-
-BIO *OSSL_HTTP_exchange(OSSL_HTTP_REQ_CTX *rctx, char **redirection_url)
-{
-    return NULL; /* TODO(3.0) expand */
-}
-
-BIO *OSSL_HTTP_transfer(OSSL_HTTP_REQ_CTX **prctx,
-                        const char *server, const char *port, const char *path,
-                        int use_ssl, const char *proxy, const char *no_proxy,
-                        BIO *bio, BIO *rbio,
-                        OSSL_HTTP_bio_cb_t bio_update_fn, void *arg,
-                        int maxline, const STACK_OF(CONF_VALUE) *headers,
-                        const char *content_type, BIO *req_mem,
-                        const char *expected_ct, int expect_asn1,
-                        size_t max_resp_len, int timeout, int keep_alive)
-{
-    char **redirection_url = (char **)prctx; /* TODO(3.0) fix when API approved */
-    time_t start_time = timeout > 0 ? time(NULL) : 0;
-    BIO *cbio; /* = bio if present, used as connection BIO if rbio is NULL */
-    OSSL_HTTP_REQ_CTX *rctx;
-    BIO *resp = NULL;
-
-    if (redirection_url != NULL)
-        *redirection_url = NULL; /* do this beforehand to prevent dbl free */
+    BIO *cbio; /* == bio if supplied, used as connection BIO if rbio is NULL */
+    OSSL_HTTP_REQ_CTX *rctx = NULL;
 
     if (use_ssl && bio_update_fn == NULL) {
         ERR_raise(ERR_LIB_HTTP, HTTP_R_TLS_NOT_ENABLED);
@@ -881,6 +874,10 @@ BIO *OSSL_HTTP_transfer(OSSL_HTTP_REQ_CTX **prctx,
 
     if (bio != NULL) {
         cbio = bio;
+        if (proxy != NULL || no_proxy != NULL) {
+            ERR_raise(ERR_LIB_HTTP, ERR_R_PASSED_INVALID_ARGUMENT);
+            return NULL;
+        }
     } else {
 #ifndef OPENSSL_NO_SOCK
         char *proxy_host = NULL, *proxy_port = NULL;
@@ -889,7 +886,7 @@ BIO *OSSL_HTTP_transfer(OSSL_HTTP_REQ_CTX **prctx,
             ERR_raise(ERR_LIB_HTTP, ERR_R_PASSED_NULL_PARAMETER);
             return NULL;
         }
-        if (*port == '\0')
+        if (port != NULL && *port == '\0')
             port = NULL;
         if (port == NULL && strchr(server, ':') == NULL)
             port = use_ssl ? OSSL_HTTPS_PORT : OSSL_HTTP_PORT;
@@ -899,7 +896,7 @@ BIO *OSSL_HTTP_transfer(OSSL_HTTP_REQ_CTX **prctx,
                                     &proxy_host, &proxy_port, NULL /* num */,
                                     NULL /* path */, NULL, NULL))
             return NULL;
-        cbio = HTTP_new_bio(server, port, use_ssl, proxy_host, proxy_port);
+        cbio = http_new_bio(server, port, use_ssl, proxy_host, proxy_port);
         OPENSSL_free(proxy_host);
         OPENSSL_free(proxy_port);
         if (cbio == NULL)
@@ -909,16 +906,19 @@ BIO *OSSL_HTTP_transfer(OSSL_HTTP_REQ_CTX **prctx,
         return NULL;
 #endif
     }
-    /* remaining parameters are checked indirectly by the functions called */
 
     (void)ERR_set_mark(); /* prepare removing any spurious libssl errors */
-    if (rbio == NULL && BIO_do_connect_retry(cbio, timeout, -1) <= 0)
+    if (rbio == NULL && BIO_do_connect_retry(cbio, overall_timeout, -1) <= 0) {
+        if (bio == NULL) /* cbio was not provided by caller */
+            BIO_free_all(cbio);
         goto end;
-    /* now timeout is guaranteed to be >= 0 */
+    }
+    /* now overall_timeout is guaranteed to be >= 0 */
 
     /* callback can be used to wrap or prepend TLS session */
     if (bio_update_fn != NULL) {
         BIO *orig_bio = cbio;
+
         cbio = (*bio_update_fn)(cbio, arg, 1 /* connect */, use_ssl);
         if (cbio == NULL) {
             cbio = orig_bio;
@@ -926,13 +926,64 @@ BIO *OSSL_HTTP_transfer(OSSL_HTTP_REQ_CTX **prctx,
         }
     }
 
-    rctx = ossl_http_req_ctx_new(cbio, rbio != NULL ? rbio : cbio,
-                                 !use_ssl && proxy != NULL, server, port, path,
-                                 headers, content_type, req_mem, maxline,
-                                 update_timeout(timeout, start_time),
-                                 expected_ct, expect_asn1);
-    if (rctx == NULL)
-        goto end;
+    rctx = http_req_ctx_new(bio == NULL, cbio, rbio != NULL ? rbio : cbio,
+                            bio_update_fn, arg, use_ssl, proxy, server, port,
+                            buf_size, overall_timeout);
+
+ end:
+    if (rctx != NULL)
+        /* remove any spurious error queue entries by ssl_add_cert_chain() */
+        (void)ERR_pop_to_mark();
+    else
+        (void)ERR_clear_last_mark();
+
+    return rctx;
+}
+
+int OSSL_HTTP_set_request(OSSL_HTTP_REQ_CTX *rctx, const char *path,
+                          const STACK_OF(CONF_VALUE) *headers,
+                          const char *content_type, BIO *req,
+                          const char *expected_content_type, int expect_asn1,
+                          size_t max_resp_len, int timeout, int keep_alive)
+{
+    int use_http_proxy;
+
+    if (rctx == NULL) {
+        ERR_raise(ERR_LIB_HTTP, ERR_R_PASSED_NULL_PARAMETER);
+        return 0;
+    }
+    use_http_proxy = rctx->proxy != NULL && !rctx->use_ssl;
+    if (use_http_proxy && (rctx->server == NULL || rctx->port == NULL)) {
+        ERR_raise(ERR_LIB_HTTP, ERR_R_PASSED_INVALID_ARGUMENT);
+        return 0;
+    }
+    rctx->max_resp_len = max_resp_len; /* allows for 0: indefinite */
+
+    return OSSL_HTTP_REQ_CTX_set_request_line(rctx, req != NULL,
+                                              use_http_proxy ? rctx->server
+                                              : NULL, rctx->port, path)
+        && add1_headers(rctx, headers, rctx->server)
+        && OSSL_HTTP_REQ_CTX_set_expected(rctx, expected_content_type,
+                                          expect_asn1, timeout, keep_alive)
+        && set_content(rctx, content_type, req);
+}
+
+/*-
+ * Exchange single HTTP request and response according to rctx.
+ * If rctx->method_POST then use POST, else use GET and ignore content_type.
+ * The redirection_url output (freed by caller) parameter is used only for GET.
+ */
+BIO *OSSL_HTTP_exchange(OSSL_HTTP_REQ_CTX *rctx, char **redirection_url)
+{
+    BIO *resp;
+
+    if (rctx == NULL) {
+        ERR_raise(ERR_LIB_HTTP, ERR_R_PASSED_NULL_PARAMETER);
+        return NULL;
+    }
+
+    if (redirection_url != NULL)
+        *redirection_url = NULL; /* do this beforehand to prevent dbl free */
 
     resp = OSSL_HTTP_REQ_CTX_exchange(rctx);
     if (resp == NULL) {
@@ -956,43 +1007,27 @@ BIO *OSSL_HTTP_transfer(OSSL_HTTP_REQ_CTX **prctx,
                         && reason == CMP_R_POTENTIALLY_INVALID_CERTIFICATE)
 #endif
                 ) {
-                BIO_snprintf(buf, 200, "server=%s:%s", server, port);
-                ERR_add_error_data(1, buf);
-                if (proxy != NULL)
-                    ERR_add_error_data(2, " proxy=", proxy);
+                if (rctx->server != NULL) {
+                    BIO_snprintf(buf, sizeof(buf), "server=http%s://%s%s%s",
+                                 rctx->use_ssl ? "s" : "", rctx->server,
+                                 rctx->port != NULL ? ":" : "",
+                                 rctx->port != NULL ? rctx->port : "");
+                    ERR_add_error_data(1, buf);
+                }
+                if (rctx->proxy != NULL)
+                    ERR_add_error_data(2, " proxy=", rctx->proxy);
                 if (err == 0) {
-                    BIO_snprintf(buf, 200, " peer has disconnected%s",
-                                 use_ssl ? " violating the protocol" :
+                    BIO_snprintf(buf, sizeof(buf), " peer has disconnected%s",
+                                 rctx->use_ssl ? " violating the protocol" :
                                  ", likely because it requires the use of TLS");
                     ERR_add_error_data(1, buf);
                 }
             }
         }
     }
-    /* callback can be used to clean up TLS session */
-    if (bio_update_fn != NULL
-            && (*bio_update_fn)(cbio, arg, 0, resp != NULL) == NULL)
-        resp = NULL;
 
     if (resp != NULL && !BIO_up_ref(resp))
         resp = NULL;
-    OSSL_HTTP_REQ_CTX_free(rctx);
-
- end:
-    /*
-     * Use BIO_free_all() because bio_update_fn may prepend or append to cbio.
-     * This also frees any (e.g., SSL/TLS) BIOs linked with bio and,
-     * like BIO_reset(bio), calls SSL_shutdown() to notify/alert the peer.
-     */
-    if (bio == NULL) /* cbio was not provided by caller */
-        BIO_free_all(cbio);
-
-    if (resp != NULL)
-        /* remove any spurious error queue entries by ssl_add_cert_chain() */
-        (void)ERR_pop_to_mark();
-    else
-        (void)ERR_clear_last_mark();
-
     return resp;
 }
 
@@ -1018,17 +1053,17 @@ static int redirection_ok(int n_redir, const char *old_url, const char *new_url)
 BIO *OSSL_HTTP_get(const char *url, const char *proxy, const char *no_proxy,
                    BIO *bio, BIO *rbio,
                    OSSL_HTTP_bio_cb_t bio_update_fn, void *arg,
-                   int maxline, const STACK_OF(CONF_VALUE) *headers,
+                   int buf_size, const STACK_OF(CONF_VALUE) *headers,
                    const char *expected_ct, int expect_asn1,
                    size_t max_resp_len, int timeout)
 {
-    time_t start_time = timeout > 0 ? time(NULL) : 0;
     char *current_url, *redirection_url = NULL;
     int n_redirs = 0;
     char *host;
     char *port;
     char *path;
     int use_ssl;
+    OSSL_HTTP_REQ_CTX *rctx;
     BIO *resp = NULL;
 
     if (url == NULL) {
@@ -1043,14 +1078,21 @@ BIO *OSSL_HTTP_get(const char *url, const char *proxy, const char *no_proxy,
                                  &port, NULL /* port_num */, &path, NULL, NULL))
             break;
 
-     new_rpath:
-        resp = OSSL_HTTP_transfer((OSSL_HTTP_REQ_CTX **)&redirection_url, /* TODO(3.0) fix when API approved */
-                                  host, port, path, use_ssl, proxy, no_proxy,
-                                  bio, rbio,
-                                  bio_update_fn, arg, maxline, headers, NULL, NULL,
-                                  expected_ct, expect_asn1,
-                                  max_resp_len,
-                                  update_timeout(timeout, start_time), 0);
+        rctx = OSSL_HTTP_open(host, port, proxy, no_proxy,
+                              use_ssl, bio, rbio, bio_update_fn, arg,
+                              buf_size, timeout);
+    new_rpath:
+        if (rctx != NULL) {
+            if (!OSSL_HTTP_set_request(rctx, path, headers,
+                                       NULL /* content_type */,
+                                       NULL /* req */,
+                                       expected_ct, expect_asn1, max_resp_len,
+                                       -1 /* use same max time (timeout) */,
+                                       0 /* no keep_alive */))
+                OSSL_HTTP_REQ_CTX_free(rctx);
+            else
+                resp = OSSL_HTTP_exchange(rctx, &redirection_url);
+        }
         OPENSSL_free(path);
         if (resp == NULL && redirection_url != NULL) {
             if (redirection_ok(++n_redirs, current_url, redirection_url)) {
@@ -1063,21 +1105,72 @@ BIO *OSSL_HTTP_get(const char *url, const char *proxy, const char *no_proxy,
                 }
                 OPENSSL_free(host);
                 OPENSSL_free(port);
+                (void)OSSL_HTTP_close(rctx, 1);
                 continue;
             }
+            /* if redirection not allowed, ignore it */
             OPENSSL_free(redirection_url);
         }
         OPENSSL_free(host);
         OPENSSL_free(port);
+        if (!OSSL_HTTP_close(rctx, resp != NULL)) {
+            BIO_free(resp);
+            resp = NULL;
+        }
         break;
     }
     OPENSSL_free(current_url);
     return resp;
 }
 
+/* Exchange request and response over a connection managed via |prctx| */
+BIO *OSSL_HTTP_transfer(OSSL_HTTP_REQ_CTX **prctx,
+                        const char *server, const char *port,
+                        const char *path, int use_ssl,
+                        const char *proxy, const char *no_proxy,
+                        BIO *bio, BIO *rbio,
+                        OSSL_HTTP_bio_cb_t bio_update_fn, void *arg,
+                        int buf_size, const STACK_OF(CONF_VALUE) *headers,
+                        const char *content_type, BIO *req,
+                        const char *expected_ct, int expect_asn1,
+                        size_t max_resp_len, int timeout, int keep_alive)
+{
+    OSSL_HTTP_REQ_CTX *rctx = prctx == NULL ? NULL : *prctx;
+    BIO *resp = NULL;
+
+    if (rctx == NULL) {
+        rctx = OSSL_HTTP_open(server, port, proxy, no_proxy,
+                              use_ssl, bio, rbio, bio_update_fn, arg,
+                              buf_size, timeout);
+        timeout = -1; /* Already set during opening the connection */
+    }
+    if (rctx != NULL) {
+        if (OSSL_HTTP_set_request(rctx, path, headers, content_type, req,
+                                  expected_ct, expect_asn1,
+                                  max_resp_len, timeout, keep_alive))
+            resp = OSSL_HTTP_exchange(rctx, NULL);
+        if (resp == NULL || !OSSL_HTTP_is_alive(rctx)) {
+            if (!OSSL_HTTP_close(rctx, resp != NULL)) {
+                BIO_free(resp);
+                resp = NULL;
+            }
+            rctx = NULL;
+        }
+    }
+    if (prctx != NULL)
+        *prctx = rctx;
+    return resp;
+}
+
 int OSSL_HTTP_close(OSSL_HTTP_REQ_CTX *rctx, int ok)
 {
-    return 0; /* TODO(3.0) expand */
+    int ret = 1;
+
+    /* callback can be used to clean up TLS session on disconnect */
+    if (rctx != NULL && rctx->upd_fn != NULL)
+        ret = (*rctx->upd_fn)(rctx->wbio, rctx->upd_arg, 0, ok) != NULL;
+    OSSL_HTTP_REQ_CTX_free(rctx);
+    return ret;
 }
 
 /* BASE64 encoder used for encoding basic proxy authentication credentials */
@@ -1137,7 +1230,7 @@ int OSSL_HTTP_proxy_connect(BIO *bio, const char *server, const char *port,
     }
     BIO_push(fbio, bio);
 
-    BIO_printf(fbio, "CONNECT %s:%s "HTTP_PREFIX"1.0\r\n", server, port);
+    BIO_printf(fbio, "CONNECT %s:%s "HTTP_1_0"\r\n", server, port);
 
     /*
      * Workaround for broken proxies which would otherwise close
@@ -1199,23 +1292,25 @@ int OSSL_HTTP_proxy_connect(BIO *bio, const char *server, const char *port,
         if (read_len < HTTP_LINE1_MINLEN)
             continue;
 
-        /* RFC 7231 4.3.6: any 2xx status code is valid */
+        /* Check for HTTP/1.x */
         if (strncmp(mbuf, HTTP_PREFIX, strlen(HTTP_PREFIX)) != 0) {
-            ERR_raise(ERR_LIB_HTTP, HTTP_R_RESPONSE_PARSE_ERROR);
+            ERR_raise(ERR_LIB_HTTP, HTTP_R_HEADER_PARSE_ERROR);
             BIO_printf(bio_err, "%s: HTTP CONNECT failed, non-HTTP response\n",
                        prog);
             /* Wrong protocol, not even HTTP, so stop reading headers */
             goto end;
         }
         mbufp = mbuf + strlen(HTTP_PREFIX);
-        if (strncmp(mbufp, HTTP_VERSION_PATT, strlen(HTTP_VERSION_PATT)) != 0) {
+        if (strncmp(mbufp, HTTP_VERSION_PATT, HTTP_VERSION_PATT_LEN) != 0) {
             ERR_raise(ERR_LIB_HTTP, HTTP_R_RECEIVED_WRONG_HTTP_VERSION);
             BIO_printf(bio_err,
                        "%s: HTTP CONNECT failed, bad HTTP version %.*s\n",
-                       prog, HTTP_VERSION_STR_LEN, mbufp);
+                       prog, (int)HTTP_VERSION_STR_LEN, mbufp);
             goto end;
         }
         mbufp += HTTP_VERSION_STR_LEN;
+
+        /* RFC 7231 4.3.6: any 2xx status code is valid */
         if (strncmp(mbufp, " 2", strlen(" 2")) != 0) {
             mbufp += 1;
             /* chop any trailing whitespace */
diff --git a/crypto/http/http_err.c b/crypto/http/http_err.c
index 2bb6d97290..4ac639197e 100644
--- a/crypto/http/http_err.c
+++ b/crypto/http/http_err.c
@@ -27,6 +27,8 @@ static const ERR_STRING_DATA HTTP_str_reasons[] = {
     {ERR_PACK(ERR_LIB_HTTP, 0, HTTP_R_ERROR_SENDING), "error sending"},
     {ERR_PACK(ERR_LIB_HTTP, 0, HTTP_R_FAILED_READING_DATA),
     "failed reading data"},
+    {ERR_PACK(ERR_LIB_HTTP, 0, HTTP_R_HEADER_PARSE_ERROR),
+    "header parse error"},
     {ERR_PACK(ERR_LIB_HTTP, 0, HTTP_R_INCONSISTENT_CONTENT_LENGTH),
     "inconsistent content length"},
     {ERR_PACK(ERR_LIB_HTTP, 0, HTTP_R_INVALID_PORT_NUMBER),
@@ -53,6 +55,8 @@ static const ERR_STRING_DATA HTTP_str_reasons[] = {
     "response line too long"},
     {ERR_PACK(ERR_LIB_HTTP, 0, HTTP_R_RESPONSE_PARSE_ERROR),
     "response parse error"},
+    {ERR_PACK(ERR_LIB_HTTP, 0, HTTP_R_SERVER_CANCELED_CONNECTION),
+    "server canceled connection"},
     {ERR_PACK(ERR_LIB_HTTP, 0, HTTP_R_SOCK_NOT_SUPPORTED),
     "sock not supported"},
     {ERR_PACK(ERR_LIB_HTTP, 0, HTTP_R_STATUS_CODE_UNSUPPORTED),
diff --git a/crypto/ocsp/ocsp_http.c b/crypto/ocsp/ocsp_http.c
index 8cf816e53f..f19047aa08 100644
--- a/crypto/ocsp/ocsp_http.c
+++ b/crypto/ocsp/ocsp_http.c
@@ -14,16 +14,25 @@
 #ifndef OPENSSL_NO_OCSP
 
 OSSL_HTTP_REQ_CTX *OCSP_sendreq_new(BIO *io, const char *path,
-                                    const OCSP_REQUEST *req, int maxline)
+                                    const OCSP_REQUEST *req, int buf_size)
 {
-    OSSL_HTTP_REQ_CTX *rctx = OSSL_HTTP_REQ_CTX_new(io, io, maxline);
+    OSSL_HTTP_REQ_CTX *rctx = OSSL_HTTP_REQ_CTX_new(io, io, buf_size);
 
     if (rctx == NULL)
         return NULL;
-
-    if (!OSSL_HTTP_REQ_CTX_set_request_line(rctx, 1 /* POST */, NULL, NULL, path))
+    /*-
+     * by default:
+     * no bio_update_fn (and consequently no arg)
+     * no ssl
+     * no proxy
+     * no timeout (blocking indefinitely)
+     * no expected content type
+     * max_resp_len = 100 KiB
+     */
+    if (!OSSL_HTTP_REQ_CTX_set_request_line(rctx, 1 /* POST */,
+                                            NULL, NULL, path))
         goto err;
-
+    /* by default, no extra headers */
     if (!OSSL_HTTP_REQ_CTX_set_expected(rctx,
                                         NULL /* content_type */, 1 /* asn1 */,
                                         0 /* timeout */, 0 /* keep_alive */))
@@ -31,9 +40,8 @@ OSSL_HTTP_REQ_CTX *OCSP_sendreq_new(BIO *io, const char *path,
     if (req != NULL
         && !OSSL_HTTP_REQ_CTX_set1_req(rctx, "application/ocsp-request",
                                        ASN1_ITEM_rptr(OCSP_REQUEST),
-                                       (ASN1_VALUE *)req))
+                                       (const ASN1_VALUE *)req))
         goto err;
-
     return rctx;
 
  err:
@@ -47,7 +55,7 @@ OCSP_RESPONSE *OCSP_sendreq_bio(BIO *b, const char *path, OCSP_REQUEST *req)
     OSSL_HTTP_REQ_CTX *ctx;
     BIO *mem;
 
-    ctx = OCSP_sendreq_new(b, path, req, -1 /* default max resp line length */);
+    ctx = OCSP_sendreq_new(b, path, req, 0 /* default buf_size */);
     if (ctx == NULL)
         return NULL;
     mem = OSSL_HTTP_REQ_CTX_exchange(ctx);
diff --git a/crypto/x509/x_all.c b/crypto/x509/x_all.c
index 1bd47ce654..ba400d1103 100644
--- a/crypto/x509/x_all.c
+++ b/crypto/x509/x_all.c
@@ -79,7 +79,7 @@ static ASN1_VALUE *simple_get_asn1(const char *url, BIO *bio, BIO *rbio,
                              bio, rbio, NULL /* cb */ , NULL /* arg */,
                              1024 /* buf_size */, NULL /* headers */,
                              NULL /* expected_ct */, 1 /* expect_asn1 */,
-                             HTTP_DEFAULT_MAX_RESP_LEN, timeout);
+                             OSSL_HTTP_DEFAULT_MAX_RESP_LEN, timeout);
     ASN1_VALUE *res = ASN1_item_d2i_bio(it, mem, NULL);
 
     BIO_free(mem);
diff --git a/doc/man3/OSSL_CMP_CTX_new.pod b/doc/man3/OSSL_CMP_CTX_new.pod
index 4260c04d88..51ac68d1a7 100644
--- a/doc/man3/OSSL_CMP_CTX_new.pod
+++ b/doc/man3/OSSL_CMP_CTX_new.pod
@@ -194,10 +194,20 @@ The following options can be set:
         due to errors, warnings, general info, debugging, etc.
         Default is OSSL_CMP_LOG_INFO. See also L<OSSL_CMP_log_open(3)>.
 
+=item B<OSSL_CMP_OPT_KEEP_ALIVE>
+
+        If the given value is 0 then HTTP connections are not kept open
+        after receiving a response, which is the default behavior for HTTP 1.0.
+        If the value is 1 or 2 then persistent connections are requested.
+        If the value is 2 then persistent connections are required,
+        i.e., in case the server does not grant them an error occurs.
+        The default value is 1: prefer to keep the connection open.
+
 =item B<OSSL_CMP_OPT_MSG_TIMEOUT>
 
         Number of seconds (or 0 for infinite) a CMP message round trip is
-        allowed to take before a timeout error is returned. Default is 120.
+        allowed to take before a timeout error is returned.
+        Default is to use the B<OSSL_CMP_OPT_MSG_TIMEOUT> setting.
 
 =item B<OSSL_CMP_OPT_TOTAL_TIMEOUT>
 
@@ -602,6 +612,7 @@ OSSL_CMP_CTX_set_certConf_cb_arg(), or NULL if unset.
 
 OSSL_CMP_CTX_get_status() returns the PKIstatus from the last received
 CertRepMessage or Revocation Response or error message, or -1 if unset.
+For server contexts it returns -2 if a transaction is open, else -1.
 
 OSSL_CMP_CTX_get0_statusString() returns the statusString from the last received
 CertRepMessage or Revocation Response or error message, or NULL if unset.
diff --git a/doc/man3/OSSL_CMP_SRV_CTX_new.pod b/doc/man3/OSSL_CMP_SRV_CTX_new.pod
index adce88547b..bad043cb92 100644
--- a/doc/man3/OSSL_CMP_SRV_CTX_new.pod
+++ b/doc/man3/OSSL_CMP_SRV_CTX_new.pod
@@ -89,6 +89,10 @@ Its arguments are the B<OSSL_CMP_SRV_CTX> I<srv_ctx> and the CMP request message
 I<req>. It does the typical generic checks on I<req>, calls
 the respective callback function (if present) for more specific processing,
 and then assembles a result message, which may be a CMP error message.
+If after return of the function the expression
+I<OSSL_CMP_CTX_get_status(OSSL_CMP_SRV_CTX_get0_cmp_ctx(srv_ctx))> yields -1
+then the function has closed the current transaction,
+which may be due to normal successful end of the transaction or due to an error.
 
 OSSL_CMP_CTX_server_perform() is an interface to
 OSSL_CMP_SRV_process_request() that can be used by a CMP client
diff --git a/doc/man3/OSSL_HTTP_REQ_CTX.pod b/doc/man3/OSSL_HTTP_REQ_CTX.pod
index a09b9b81a9..ec358d265f 100644
--- a/doc/man3/OSSL_HTTP_REQ_CTX.pod
+++ b/doc/man3/OSSL_HTTP_REQ_CTX.pod
@@ -64,7 +64,7 @@ which gets populated with the B<BIO> to write/send the request to (I<wbio>),
 the B<BIO> to read/receive the response from (I<rbio>, which may be equal to
 I<wbio>), and the maximum expected response header line length I<buf_size>.
 A value <= 0 indicates that
-the B<HTTP_DEFAULT_MAX_LINE_LENGTH> of 4KiB should be used.
+the B<OSSL_HTTP_DEFAULT_MAX_LINE_LEN> of 4KiB should be used.
 I<buf_size> is also used as the number of content bytes that are read at a time.
 The allocated context structure is also populated with an internal allocated
 memory B<BIO>, which collects the HTTP request and additional headers as text.
@@ -154,7 +154,7 @@ in I<rctx> if provided by the server as <Content-Length> header field, else 0.
 
 OSSL_HTTP_REQ_CTX_set_max_response_length() sets the maximum allowed
 response content length for I<rctx> to I<len>. If not set or I<len> is 0
-then the B<HTTP_DEFAULT_MAX_RESP_LEN> is used, which currently is 100 KiB.
+then the B<OSSL_HTTP_DEFAULT_MAX_RESP_LEN> is used, which currently is 100 KiB.
 If the C<Content-Length> header is present and exceeds this value or
 the content is an ASN.1 encoded structure with a length exceeding this value
 or both length indications are present but disagree then an error occurs.
@@ -222,7 +222,7 @@ OSSL_HTTP_REQ_CTX_nbio() and OSSL_HTTP_REQ_CTX_nbio_d2i()
 return 1 for success, 0 on error or redirection, -1 if retry is needed.
 
 OSSL_HTTP_REQ_CTX_exchange() and OSSL_HTTP_REQ_CTX_get0_mem_bio()
-returns a pointer to a B<BIO> on success and NULL on failure.
+return a pointer to a B<BIO> on success and NULL on failure.
 
 OSSL_HTTP_REQ_CTX_get_resp_len() returns the size of the response contents
 or 0 if not available or an error occurred.
diff --git a/doc/man3/OSSL_HTTP_parse_url.pod b/doc/man3/OSSL_HTTP_parse_url.pod
index 60589b6bf9..559ff1dd08 100644
--- a/doc/man3/OSSL_HTTP_parse_url.pod
+++ b/doc/man3/OSSL_HTTP_parse_url.pod
@@ -31,7 +31,7 @@ L<openssl_user_macros(7)>:
 OSSL_parse_url() parses its input string I<url> as a URL of the form
 C<[scheme://][userinfo@]host[:port][/path][?query][#fragment]> and splits it up
 into scheme, userinfo, host, port, path, query, and fragment components.
-The host component may be a DNS name or an IP address
+The host (or server) component may be a DNS name or an IP address
 where IPv6 addresses should be enclosed in square brackets C<[> and C<]>.
 The port component is optional and defaults to C<0>.
 If given, it must be in decimal form.  If the I<pport_num> argument is not NULL
@@ -52,6 +52,8 @@ If I<pssl> is not NULL, I<*pssl> is assigned 1 in case parsing was successful
 and the scheme is C<https>, else 0.
 The port component is optional and defaults to C<443> if the scheme is C<https>,
 else C<80>.
+Note that relative paths must be given with a leading C</>,
+otherwise the first path element is interpreted as the hostname.
 
 Calling the deprecated function OCSP_parse_url(url, host, port, path, ssl)
 is equivalent to
@@ -59,7 +61,7 @@ OSSL_HTTP_parse_url(url, ssl, NULL, host, port, NULL, path, NULL, NULL).
 
 =head1 RETURN VALUES
 
-OSSL_HTTP_parse_url() and OCSP_parse_url()
+OSSL_parse_url(), OSSL_HTTP_parse_url(), and OCSP_parse_url()
 return 1 on success, 0 on error.
 
 =head1 SEE ALSO
@@ -68,7 +70,7 @@ L<OSSL_HTTP_transfer(3)>
 
 =head1 HISTORY
 
-OOSSL_HTTP_parse_url() was added in OpenSSL 3.0.
+OSSL_parse_url() and OSSL_HTTP_parse_url() were added in OpenSSL 3.0.
 OCSP_parse_url() was deprecated in OpenSSL 3.0.
 
 =head1 COPYRIGHT
diff --git a/doc/man3/OSSL_HTTP_transfer.pod b/doc/man3/OSSL_HTTP_transfer.pod
index da84789472..d6eb39f652 100644
--- a/doc/man3/OSSL_HTTP_transfer.pod
+++ b/doc/man3/OSSL_HTTP_transfer.pod
@@ -123,8 +123,7 @@ Here is a simple example that supports TLS connections (but not via a proxy):
 After disconnect the modified BIO will be deallocated using BIO_free_all().
 
 The I<buf_size> parameter specifies the response header maximum line length.
-A value <= 0 indicates that
-the B<HTTP_DEFAULT_MAX_LINE_LENGTH> of 4KiB should be used.
+A value <= 0 means that the B<OSSL_HTTP_DEFAULT_MAX_LINE_LEN> (4KiB) is used.
 I<buf_size> is also used as the number of content bytes that are read at a time.
 
 If the I<overall_timeout> parameter is > 0 this indicates the maximum number of
diff --git a/include/openssl/http.h b/include/openssl/http.h
index 2140d5d2f8..76d20c5242 100644
--- a/include/openssl/http.h
+++ b/include/openssl/http.h
@@ -33,8 +33,8 @@ extern "C" {
 # define OPENSSL_HTTP_PROXY "HTTP_PROXY"
 # define OPENSSL_HTTPS_PROXY "HTTPS_PROXY"
 
-#define HTTP_DEFAULT_MAX_LINE_LENGTH (4 * 1024)
-#define HTTP_DEFAULT_MAX_RESP_LEN (100 * 1024)
+#define OSSL_HTTP_DEFAULT_MAX_LINE_LEN (4 * 1024)
+#define OSSL_HTTP_DEFAULT_MAX_RESP_LEN (100 * 1024)
 
 /* Low-level HTTP API */
 OSSL_HTTP_REQ_CTX *OSSL_HTTP_REQ_CTX_new(BIO *wbio, BIO *rbio, int buf_size);
diff --git a/test/http_test.c b/test/http_test.c
index e4209a37c0..b9f7452744 100644
--- a/test/http_test.c
+++ b/test/http_test.c
@@ -17,26 +17,30 @@
 
 static const ASN1_ITEM *x509_it = NULL;
 static X509 *x509 = NULL;
-#define SERVER "mock.server"
-#define PORT   "81"
-#define RPATH  "path/any.crt"
-static const char *rpath;
-
-/*
- * pretty trivial HTTP mock server:
- * for POST, copy request headers+body from mem BIO 'in' as response to 'out'
- * for GET, first redirect the request then respond with 'rsp' of ASN1 type 'it'
+#define RPATH "/path/result.crt"
+
+typedef struct {
+    BIO *out;
+    char version;
+    int keep_alive;
+} server_args;
+
+/*-
+ * Pretty trivial HTTP mock server:
+ * For POST, copy request headers+body from mem BIO 'in' as response to 'out'.
+ * For GET, redirect to RPATH, else respond with 'rsp' of ASN1 type 'it'.
+ * Respond with HTTP version 1.'version' and 'keep_alive' (unless implicit).
  */
-static int mock_http_server(BIO *in, BIO *out,
+static int mock_http_server(BIO *in, BIO *out, char version, int keep_alive,
                             ASN1_VALUE *rsp, const ASN1_ITEM *it)
 {
-    const char *req;
+    const char *req, *path;
     long count = BIO_get_mem_data(in, (unsigned char **)&req);
     const char *hdr = (char *)req;
     int is_get = count >= 4 && strncmp(hdr, "GET ", 4) == 0;
     int len;
 
-    /* first line should contain "<GET or POST> <rpath> HTTP/1.x" */
+    /* first line should contain "<GET or POST> <path> HTTP/1.x" */
     if (is_get)
         hdr += 4;
     else if (TEST_true(count >= 5 && strncmp(hdr, "POST ", 5) == 0))
@@ -44,16 +48,12 @@ static int mock_http_server(BIO *in, BIO *out,
     else
         return 0;
 
-    while (*rpath == '/')
-        rpath++;
-    while (*hdr == '/')
-        hdr++;
-    len = strlen(rpath);
-    if (!TEST_strn_eq(hdr, rpath, len) || !TEST_char_eq(hdr++[len], ' '))
+    path = hdr;
+    hdr = strchr(hdr, ' ');
+    if (hdr == NULL)
         return 0;
-    hdr += len;
     len = strlen("HTTP/1.");
-    if (!TEST_strn_eq(hdr, "HTTP/1.", len))
+    if (!TEST_strn_eq(++hdr, "HTTP/1.", len))
         return 0;
     hdr += len;
     /* check for HTTP version 1.0 .. 1.1 */
@@ -62,16 +62,22 @@ static int mock_http_server(BIO *in, BIO *out,
     if (!TEST_char_eq(*hdr++, '\r') || !TEST_char_eq(*hdr++, '\n'))
         return 0;
     count -= (hdr - req);
-    if (count <= 0 || out == NULL)
+    if (count < 0 || out == NULL)
         return 0;
 
-    if (is_get && strcmp(rpath, RPATH) == 0) {
-        rpath = "path/new.crt";
-        return BIO_printf(out, "HTTP/1.1 301 Moved Permanently\r\n"
-                          "Location: /%s\r\n\r\n", rpath) > 0; /* same server */
+    if (strncmp(path, RPATH, strlen(RPATH)) != 0) {
+        if (!is_get)
+            return 0;
+        return BIO_printf(out, "HTTP/1.%c 301 Moved Permanently\r\n"
+                          "Location: %s\r\n\r\n",
+                          version, RPATH) > 0; /* same server */
     }
-    if (BIO_printf(out, "HTTP/1.1 200 OK\r\n") <= 0)
+    if (BIO_printf(out, "HTTP/1.%c 200 OK\r\n", version) <= 0)
         return 0;
+    if ((version == '0') == keep_alive) /* otherwise, default */
+        if (BIO_printf(out, "Connection: %s\r\n",
+                       version == '0' ? "keep-alive" : "close") <= 0)
+            return 0;
     if (is_get) { /* construct new header and body */
         if ((len = ASN1_item_i2d(rsp, NULL, it)) <= 0)
             return 0;
@@ -80,16 +86,26 @@ static int mock_http_server(BIO *in, BIO *out,
             return 0;
         return ASN1_item_i2d_bio(it, out, rsp);
     } else {
-        return BIO_write(out, hdr, count) == count; /* echo header and body */
+        len = strlen("Connection: ");
+        if (strncmp(hdr, "Connection: ", len) == 0) {
+            /* skip req Connection header */
+            hdr = strstr(hdr + len, "\r\n");
+            if (hdr == NULL)
+                return 0;
+            hdr += 2;
+        }
+        /* echo remaining request header and body */
+        return BIO_write(out, hdr, count) == count;
     }
 }
 
 static long http_bio_cb_ex(BIO *bio, int oper, const char *argp, size_t len,
                            int cmd, long argl, int ret, size_t *processed)
 {
+    server_args *args = (server_args *)BIO_get_callback_arg(bio);
 
     if (oper == (BIO_CB_CTRL | BIO_CB_RETURN) && cmd == BIO_CTRL_FLUSH)
-        ret = mock_http_server(bio, (BIO *)BIO_get_callback_arg(bio),
+        ret = mock_http_server(bio, args->out, args->version, args->keep_alive,
                                (ASN1_VALUE *)x509, x509_it);
     return ret;
 }
@@ -99,6 +115,7 @@ static int test_http_x509(int do_get)
     X509 *rcert = NULL;
     BIO *wbio = BIO_new(BIO_s_mem());
     BIO *rbio = BIO_new(BIO_s_mem());
+    server_args mock_args = { NULL, '0', 0 };
     BIO *rsp, *req = ASN1_item_i2d_mem_bio(x509_it, (ASN1_VALUE *)x509);
     STACK_OF(CONF_VALUE) *headers = NULL;
     const char content_type[] = "application/x-x509-ca-cert";
@@ -106,23 +123,23 @@ static int test_http_x509(int do_get)
 
     if (wbio == NULL || rbio == NULL || req == NULL)
         goto err;
+    mock_args.out = rbio;
     BIO_set_callback_ex(wbio, http_bio_cb_ex);
-    BIO_set_callback_arg(wbio, (char *)rbio);
+    BIO_set_callback_arg(wbio, (char *)&mock_args);
 
-    rpath = RPATH;
     rsp = do_get ?
-        OSSL_HTTP_get("http://"SERVER":"PORT"/"RPATH,
+        OSSL_HTTP_get("/will-be-redirected",
                       NULL /* proxy */, NULL /* no_proxy */,
-                      wbio, rbio, NULL /* bio_fn */, NULL /* arg */,
+                      wbio, rbio, NULL /* bio_update_fn */, NULL /* arg */,
                       0 /* buf_size */, headers, content_type,
                       1 /* expect_asn1 */,
-                      HTTP_DEFAULT_MAX_RESP_LEN, 0 /* timeout */)
+                      OSSL_HTTP_DEFAULT_MAX_RESP_LEN, 0 /* timeout */)
         : OSSL_HTTP_transfer(NULL, NULL /* host */, NULL /* port */, RPATH,
                              0 /* use_ssl */,NULL /* proxy */, NULL /* no_pr */,
                              wbio, rbio, NULL /* bio_fn */, NULL /* arg */,
                              0 /* buf_size */, headers, content_type,
                              req, content_type, 1 /* expect_asn1 */,
-                             HTTP_DEFAULT_MAX_RESP_LEN, 0 /* timeout */,
+                             OSSL_HTTP_DEFAULT_MAX_RESP_LEN, 0 /* timeout */,
                              0 /* keep_alive */);
     rcert = d2i_X509_bio(rsp, NULL);
     BIO_free(rsp);
@@ -137,6 +154,52 @@ static int test_http_x509(int do_get)
     return res;
 }
 
+static int test_http_keep_alive(char version, int keep_alive, int kept_alive)
+{
+    BIO *wbio = BIO_new(BIO_s_mem());
+    BIO *rbio = BIO_new(BIO_s_mem());
+    BIO *rsp;
+    server_args mock_args = { NULL, '0', 0 };
+    const char *const content_type = "application/x-x509-ca-cert";
+    OSSL_HTTP_REQ_CTX *rctx = NULL;
+    int i, res = 0;
+
+    if (wbio == NULL || rbio == NULL)
+        goto err;
+    mock_args.out = rbio;
+    mock_args.version = version;
+    mock_args.keep_alive = kept_alive;
+    BIO_set_callback_ex(wbio, http_bio_cb_ex);
+    BIO_set_callback_arg(wbio, (char *)&mock_args);
+
+    for (res = 1, i = 1; res && i <= 2; i++) {
+        rsp = OSSL_HTTP_transfer(&rctx, NULL /* server */, NULL /* port */,
+                                 RPATH, 0 /* use_ssl */,
+                                 NULL /* proxy */, NULL /* no_proxy */,
+                                 wbio, rbio, NULL /* bio_update_fn */, NULL,
+                                 0 /* buf_size */, NULL /* headers */,
+                                 NULL /* content_type */, NULL /* req => GET */,
+                                 content_type, 0 /* ASN.1 not expected */,
+                                 0 /* max_resp_len */, 0 /* timeout */,
+                                 keep_alive);
+        if (keep_alive == 2 && kept_alive == 0)
+            res = res && TEST_ptr_null(rsp)
+                && TEST_int_eq(OSSL_HTTP_is_alive(rctx), 0);
+        else
+            res = res && TEST_ptr(rsp)
+                && TEST_int_eq(OSSL_HTTP_is_alive(rctx), keep_alive > 0);
+        BIO_free(rsp);
+        (void)BIO_reset(rbio); /* discard response contents */
+        keep_alive = 0;
+    }
+    OSSL_HTTP_close(rctx, res);
+
+ err:
+    BIO_free(wbio);
+    BIO_free(rbio);
+    return res;
+}
+
 static int test_http_url_ok(const char *url, int exp_ssl, const char *exp_host,
                             const char *exp_port, const char *exp_path)
 {
@@ -253,21 +316,61 @@ static int test_http_post_x509(void)
     return test_http_x509(0);
 }
 
+static int test_http_keep_alive_0_no_no(void)
+{
+    return test_http_keep_alive('0', 0, 0);
+}
+
+static int test_http_keep_alive_1_no_no(void)
+{
+    return test_http_keep_alive('1', 0, 0);
+}
+
+static int test_http_keep_alive_0_prefer_yes(void)
+{
+    return test_http_keep_alive('0', 1, 1);
+}
+
+static int test_http_keep_alive_1_prefer_yes(void)
+{
+    return test_http_keep_alive('1', 1, 1);
+}
+
+static int test_http_keep_alive_0_require_yes(void)
+{
+    return test_http_keep_alive('0', 2, 1);
+}
+
+static int test_http_keep_alive_1_require_yes(void)
+{
+    return test_http_keep_alive('1', 2, 1);
+}
+
+static int test_http_keep_alive_0_require_no(void)
+{
+    return test_http_keep_alive('0', 2, 0);
+}
+
+static int test_http_keep_alive_1_require_no(void)
+{
+    return test_http_keep_alive('1', 2, 0);
+}
+
 void cleanup_tests(void)
 {
     X509_free(x509);
 }
 
+OPT_TEST_DECLARE_USAGE("cert.pem\n")
+
 int setup_tests(void)
 {
-    if (!test_skip_common_options()) {
-        TEST_error("Error parsing test options\n");
+    if (!test_skip_common_options())
         return 0;
-    }
 
     x509_it = ASN1_ITEM_rptr(X509);
     if (!TEST_ptr((x509 = load_cert_pem(test_get_argument(0), NULL))))
-        return 1;
+        return 0;
 
     ADD_TEST(test_http_url_dns);
     ADD_TEST(test_http_url_path_query);
@@ -279,5 +382,13 @@ int setup_tests(void)
     ADD_TEST(test_http_url_invalid_path);
     ADD_TEST(test_http_get_x509);
     ADD_TEST(test_http_post_x509);
+    ADD_TEST(test_http_keep_alive_0_no_no);
+    ADD_TEST(test_http_keep_alive_1_no_no);
+    ADD_TEST(test_http_keep_alive_0_prefer_yes);
+    ADD_TEST(test_http_keep_alive_1_prefer_yes);
+    ADD_TEST(test_http_keep_alive_0_require_yes);
+    ADD_TEST(test_http_keep_alive_1_require_yes);
+    ADD_TEST(test_http_keep_alive_0_require_no);
+    ADD_TEST(test_http_keep_alive_1_require_no);
     return 1;
 }
diff --git a/test/recipes/80-test_cmp_http_data/test_connection.csv b/test/recipes/80-test_cmp_http_data/test_connection.csv
index 5d1700fa21..3276eb5fb3 100644
--- a/test/recipes/80-test_cmp_http_data/test_connection.csv
+++ b/test/recipes/80-test_cmp_http_data/test_connection.csv
@@ -1,43 +1,49 @@
-expected,description, -section,val, -server,val, -proxy,val, -path,val, -msg_timeout,int, -total_timeout,int, -tls_used,noarg, -no_proxy,val
-,Message transfer options:,,,,,,,,,,,,
-,,,,,,,,,,,,,,,,,,,,,,,,,
-0,default config, -section,,,,,,,,BLANK,,BLANK,,BLANK,,BLANK,
-TBD,Domain name, -section,, -server,_SERVER_CN:_SERVER_PORT,,,,
-TBD,IP address, -section,, -server,_SERVER_IP:_SERVER_PORT,,,,
-,,,,,,,,,,,,,,,,,,,,,,,,,
-1,wrong server, -section,, -server,example.com:_SERVER_PORT,,,,, -msg_timeout,1,BLANK,,BLANK,,BLANK,
-1,wrong server port, -section,, -server,_SERVER_HOST:99,,,,, -msg_timeout,1,BLANK,,BLANK,,BLANK,
-1,server default port, -section,, -server,_SERVER_HOST,,,,, -msg_timeout,1,BLANK,,BLANK,,BLANK,
-1,server port out of range, -section,, -server,_SERVER_HOST:65536,,,,,BLANK,,BLANK,,BLANK,,BLANK,
-1,server port negative, -section,, -server,_SERVER_HOST:-10,,,,,BLANK,,BLANK,,BLANK,,BLANK,
-1,server missing argument, -section,, -server,,,,,,BLANK,,BLANK,,BLANK,,BLANK,
-1,server with default port, -section,, -server,_SERVER_HOST,,,,,BLANK,,BLANK,,BLANK,,BLANK,
-1,server port bad syntax: leading garbage, -section,, -server,_SERVER_HOST:x/+80,,,,,BLANK,,BLANK,,BLANK,,BLANK,
-1,server port bad synatx: trailing garbage, -section,, -server,_SERVER_HOST:_SERVER_PORT+/x.,,,,,BLANK,,BLANK,,BLANK,,BLANK,
-1,server with TLS port, -section,, -server,_SERVER_HOST:_SERVER_TLS,,,,,BLANK,,BLANK,,BLANK,,BLANK,
-TBD,server IP address with TLS port, -section,, -server,_SERVER_IP:_SERVER_TLS,,,,,BLANK,,BLANK,,BLANK,,BLANK,
-,,,,,,,,,,,,,,,,,,,,,,,,,
-1,proxy port bad syntax: leading garbage, -section,, -server,_SERVER_HOST:_SERVER_PORT, -proxy,127.0.0.1:x*/8888,,,BLANK,,BLANK,,BLANK,,BLANK, -no_proxy,nonmatch.com,-msg_timeout,1
-1,proxy port out of range, -section,, -server,_SERVER_HOST:_SERVER_PORT, -proxy,127.0.0.1:65536,,,BLANK,,BLANK,,BLANK,,BLANK, -no_proxy,nonmatch.com,-msg_timeout,1
-1,proxy default port, -section,, -server,_SERVER_HOST:_SERVER_PORT, -proxy,127.0.0.1,,,BLANK,,BLANK,,BLANK,,BLANK, -no_proxy,nonmatch.com,-msg_timeout,1
-1,proxy missing argument, -section,, -server,_SERVER_HOST:_SERVER_PORT, -proxy,,,,BLANK,,BLANK,,BLANK,,BLANK, -no_proxy,nonmatch.com
-,,,,,,,,,,,,,,,,,,,,,,,,,
-0,path explicit, -section,, -server,_SERVER_HOST:_SERVER_PORT,,, -path,_SERVER_PATH,BLANK,,BLANK,,BLANK,,BLANK,
-0,path overrides -server path, -section,, -server,_SERVER_HOST:_SERVER_PORT/ignored,,, -path,_SERVER_PATH,BLANK,,BLANK,,BLANK,,BLANK,
-0,path default -server path, -section,, -server,_SERVER_HOST:_SERVER_PORT/_SERVER_PATH,,, -path,"""",BLANK,,BLANK,,BLANK,,BLANK,
-1,path missing argument, -section,,,,,, -path,,BLANK,,BLANK,,BLANK,,BLANK,
-1,path wrong, -section,,,,,, -path,/publicweb/cmp/example,BLANK,,BLANK,,BLANK,,BLANK,
-0,path with additional '/'s fine according to RFC 3986, -section,,,,,, -path,/_SERVER_PATH////,BLANK,,BLANK,,BLANK,,BLANK
-1,path mixed case, -section,,,,,, -path,pKiX/,BLANK,,BLANK,,BLANK,,BLANK,
-1,path upper case, -section,,,,,, -path,PKIX/,BLANK,,BLANK,,BLANK,,BLANK,
-,,,,,,,,,,,,,,,,,,,,,,,,,
-1,msg_timeout missing argument, -section,,,,,,,, -msg_timeout,,BLANK,,BLANK,,BLANK,
-1,msg_timeout negative, -section,,,,,,,, -msg_timeout,-5,BLANK,,BLANK,,BLANK,
-0,msg_timeout 5, -section,,,,,,,, -msg_timeout,5,BLANK,,BLANK,,BLANK,
-0,msg_timeout 0, -section,,,,,,,, -msg_timeout,0,BLANK,,BLANK,,BLANK,
-,,,,,,,,,,,,,,,,,,,,,,,,,
-1,total_timeout missing argument, -section,,,,,,,,BLANK,, -total_timeout,,BLANK,,BLANK,
-1,total_timeout negative, -section,,,,,,,,BLANK,, -total_timeout,-5,BLANK,,BLANK,
-0,total_timeout 10, -section,,,,,,,,BLANK,, -total_timeout,10,BLANK,,BLANK,
-0,total_timeout 0, -section,,,,,,,,BLANK,, -total_timeout,0,BLANK,,BLANK,
-,,,,,,,,,,,,,,,,,,,,,,,,,
+expected,description, -section,val, -server,val, -proxy,val, -no_proxy,val, -tls_used,noarg, -path,val, -msg_timeout,int, -total_timeout,int, -keep_alive,val
+,Message transfer options:,,,,,,,,,,,,,,,,,,
+,,,,,,,,,,,,,,,,,,,
+0,default config, -section,,,,,,,,BLANK,,,,BLANK,,BLANK,,BLANK,
+TBD,Domain name, -section,, -server,_SERVER_CN:_SERVER_PORT,,,,,,,,,,,,,,
+TBD,IP address, -section,, -server,_SERVER_IP:_SERVER_PORT,,,,,,,,,,,,,,
+,,,,,,,,,,,,,,,,,,,
+1,wrong server, -section,, -server,example.com:_SERVER_PORT,,,,,BLANK,,,, -msg_timeout,1,BLANK,,BLANK,
+1,wrong server port, -section,, -server,_SERVER_HOST:99,,,,,BLANK,,,, -msg_timeout,1,BLANK,,BLANK,
+1,server default port, -section,, -server,_SERVER_HOST,,,,,BLANK,,,, -msg_timeout,1,BLANK,,BLANK,
+1,server port out of range, -section,, -server,_SERVER_HOST:65536,,,,,BLANK,,,,BLANK,,BLANK,,BLANK,
+1,server port negative, -section,, -server,_SERVER_HOST:-10,,,,,BLANK,,,,BLANK,,BLANK,,BLANK,
+1,server missing argument, -section,, -server,,,,,,BLANK,,,,BLANK,,BLANK,,BLANK,
+1,server with default port, -section,, -server,_SERVER_HOST,,,,,BLANK,,,,BLANK,,BLANK,,BLANK,
+1,server port bad syntax: leading garbage, -section,, -server,_SERVER_HOST:x/+80,,,,,BLANK,,,,BLANK,,BLANK,,BLANK,
+1,server port bad synatx: trailing garbage, -section,, -server,_SERVER_HOST:_SERVER_PORT+/x.,,,,,BLANK,,,,BLANK,,BLANK,,BLANK,
+1,server with TLS port, -section,, -server,_SERVER_HOST:_SERVER_TLS,,,,,BLANK,,,,BLANK,,BLANK,,BLANK,
+TBD,server IP address with TLS port, -section,, -server,_SERVER_IP:_SERVER_TLS,,,,,BLANK,,,,BLANK,,BLANK,,BLANK,
+,,,,,,,,,,,,,,,,,,,
+1,proxy port bad syntax: leading garbage, -section,, -server,_SERVER_HOST:_SERVER_PORT, -proxy,127.0.0.1:x*/8888, -no_proxy,nonmatch.com,BLANK,,,,-msg_timeout,1,BLANK,,BLANK,
+1,proxy port out of range, -section,, -server,_SERVER_HOST:_SERVER_PORT, -proxy,127.0.0.1:65536, -no_proxy,nonmatch.com,BLANK,,,,-msg_timeout,1,BLANK,,BLANK,
+1,proxy default port, -section,, -server,_SERVER_HOST:_SERVER_PORT, -proxy,127.0.0.1, -no_proxy,nonmatch.com,BLANK,,,,-msg_timeout,1,BLANK,,BLANK,
+1,proxy missing argument, -section,, -server,_SERVER_HOST:_SERVER_PORT, -proxy,, -no_proxy,nonmatch.com,BLANK,,,,BLANK,,BLANK,,BLANK,
+,,,,,,,,,,,,,,,,,,,
+0,path explicit, -section,, -server,_SERVER_HOST:_SERVER_PORT,,,,,BLANK,, -path,_SERVER_PATH,BLANK,,BLANK,,BLANK,
+0,path overrides -server path, -section,, -server,_SERVER_HOST:_SERVER_PORT/ignored,,,,,BLANK,, -path,_SERVER_PATH,BLANK,,BLANK,,BLANK,
+0,path default -server path, -section,, -server,_SERVER_HOST:_SERVER_PORT/_SERVER_PATH,,,,,BLANK,, -path,"""",BLANK,,BLANK,,BLANK,
+1,path missing argument, -section,,,,,,,,BLANK,, -path,,BLANK,,BLANK,,BLANK,
+1,path wrong, -section,,,,,,,,BLANK,, -path,/publicweb/cmp/example,BLANK,,BLANK,,BLANK,
+0,path with additional '/'s fine according to RFC 3986, -section,,,,,,,,BLANK,, -path,/_SERVER_PATH////,BLANK,,BLANK,,BLANK,
+1,path mixed case, -section,,,,,,,,BLANK,, -path,pKiX/,BLANK,,BLANK,,BLANK,
+1,path upper case, -section,,,,,,,,BLANK,, -path,PKIX/,BLANK,,BLANK,,BLANK,
+,,,,,,,,,,,,,,,,,,,
+1,msg_timeout missing argument, -section,,,,,,,,BLANK,,,, -msg_timeout,,BLANK,,BLANK,
+1,msg_timeout negative, -section,,,,,,,,BLANK,,,, -msg_timeout,-5,BLANK,,BLANK,
+0,msg_timeout 5, -section,,,,,,,,BLANK,,,, -msg_timeout,5,BLANK,,BLANK,
+0,msg_timeout 0, -section,,,,,,,,BLANK,,,, -msg_timeout,0,BLANK,,BLANK,
+,,,,,,,,,,,,,,,,,,,
+1,total_timeout missing argument, -section,,,,,,,,BLANK,,,,BLANK,, -total_timeout,,BLANK,
+1,total_timeout negative, -section,,,,,,,,BLANK,,,,BLANK,, -total_timeout,-5,BLANK,
+0,total_timeout 10, -section,,,,,,,,BLANK,,,,BLANK,, -total_timeout,10,BLANK,
+0,total_timeout 0, -section,,,,,,,,BLANK,,,,BLANK,, -total_timeout,0,BLANK,
+,,,,,,,,,,,,,,,,,,,
+1,keep_alive missing argument, -section,,,,,,,,BLANK,,,,BLANK,,BLANK,, -keep_alive,
+1,keep_alive negative, -section,,,,,,,,BLANK,,,,BLANK,,BLANK,, -keep_alive,-1
+0,keep_alive 0, -section,,,,,,,,BLANK,,,,BLANK,,BLANK,, -keep_alive,0
+0,keep_alive 1, -section,,,,,,,,BLANK,,,,BLANK,,BLANK,, -keep_alive,1
+0,keep_alive 2, -section,,,,,,,,BLANK,,,,BLANK,,BLANK,, -keep_alive,2
+1,keep_alive 3, -section,,,,,,,,BLANK,,,,BLANK,,BLANK,, -keep_alive,3


More information about the openssl-commits mailing list