You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficserver.apache.org by zw...@apache.org on 2020/01/14 21:00:49 UTC

[trafficserver] 01/02: TLSv1.3 0-RTT support (#5450)

This is an automated email from the ASF dual-hosted git repository.

zwoop pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git

commit 9ebbd60bdf05404761742acfb90bbdcbd4c33cde
Author: Fei Deng <du...@gmail.com>
AuthorDate: Tue Jan 14 14:58:09 2020 -0600

    TLSv1.3 0-RTT support (#5450)
    
    * TLSv1.3 0-RTT support
    TLSv1.3 0-RTT test
    TLSv1.3 0-RTT anti-replay
    TLSv1.3 0-RTT h2
    
    (cherry picked from commit 8236813ef44e3187febe12b7996cb2cc9754f3e7)
---
 build/crypto.m4                              |  28 +++
 configure.ac                                 |   3 +
 doc/admin-guide/files/records.config.en.rst  |  18 ++
 include/tscore/ink_config.h.in               |   1 +
 iocore/net/P_SSLConfig.h                     |   6 +
 iocore/net/P_SSLNetVConnection.h             |   6 +
 iocore/net/SSLConfig.cc                      |  13 +-
 iocore/net/SSLNetVConnection.cc              |  44 +++++
 iocore/net/SSLSessionTicket.cc               |  15 +-
 iocore/net/SSLStats.cc                       |  12 +-
 iocore/net/SSLStats.h                        |   1 +
 iocore/net/SSLUtils.cc                       | 146 ++++++++++++++-
 mgmt/RecordsConfig.cc                        |   5 +-
 proxy/ProxyTransaction.cc                    |   4 +-
 proxy/ProxyTransaction.h                     |   2 +-
 proxy/hdrs/HTTP.h                            |  26 +++
 proxy/hdrs/HdrToken.cc                       |  10 +-
 proxy/hdrs/MIME.cc                           |  22 +--
 proxy/hdrs/MIME.h                            |   6 +-
 proxy/http/Http1ClientSession.cc             |   8 +-
 proxy/http/Http1ClientSession.h              |   2 +
 proxy/http/HttpSM.cc                         |  24 ++-
 proxy/http/HttpSM.h                          |   3 +-
 proxy/http/HttpTransact.cc                   |  14 ++
 proxy/http/HttpTransact.h                    |   1 +
 proxy/http2/Http2ClientSession.cc            |  25 ++-
 proxy/http2/Http2ClientSession.h             |  17 +-
 proxy/http2/Http2ConnectionState.cc          |  25 ++-
 src/traffic_quic/traffic_quic.cc             |   2 +-
 tests/gold_tests/tls/early_h1_get.txt        |   3 +
 tests/gold_tests/tls/early_h1_post.txt       |   6 +
 tests/gold_tests/tls/early_h2_get.txt        | Bin 0 -> 77 bytes
 tests/gold_tests/tls/early_h2_multi1.txt     | Bin 0 -> 172 bytes
 tests/gold_tests/tls/early_h2_multi2.txt     | Bin 0 -> 170 bytes
 tests/gold_tests/tls/early_h2_post.txt       | Bin 0 -> 77 bytes
 tests/gold_tests/tls/h2_early_decode.py      | 257 +++++++++++++++++++++++++++
 tests/gold_tests/tls/h2_early_gen.py         | 185 +++++++++++++++++++
 tests/gold_tests/tls/test-0rtt-s_client.py   |  69 +++++++
 tests/gold_tests/tls/tls_0rtt_server.test.py | 193 ++++++++++++++++++++
 39 files changed, 1160 insertions(+), 42 deletions(-)

diff --git a/build/crypto.m4 b/build/crypto.m4
index 6361955..64787b4 100644
--- a/build/crypto.m4
+++ b/build/crypto.m4
@@ -287,3 +287,31 @@ AC_DEFUN([TS_CHECK_CRYPTO_SET_CIPHERSUITES], [
   TS_ARG_ENABLE_VAR([use], [tls-set-ciphersuites])
   AC_SUBST(use_tls_set_ciphersuites)
 ])
+
+dnl
+dnl Since OpenSSL 1.1.1
+dnl
+AC_DEFUN([TS_CHECK_EARLY_DATA], [
+  _set_ciphersuites_saved_LIBS=$LIBS
+
+  TS_ADDTO(LIBS, [$OPENSSL_LIBS])
+  AC_CHECK_HEADERS(openssl/ssl.h)
+  AC_CHECK_FUNCS(
+    SSL_set_max_early_data,
+    [
+      has_tls_early_data=1
+      early_data_check=yes
+    ],
+    [
+      has_tls_early_data=0
+      early_data_check=no
+    ]
+  )
+
+  LIBS=$_set_ciphersuites_saved_LIBS
+
+  AC_MSG_CHECKING([for OpenSSL early data support])
+  AC_MSG_RESULT([$early_data_check])
+
+  AC_SUBST(has_tls_early_data)
+])
diff --git a/configure.ac b/configure.ac
index c03c6df..6b46a1c 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1292,6 +1292,9 @@ TS_CHECK_CRYPTO_OCSP
 # Check for SSL_CTX_set_ciphersuites call
 TS_CHECK_CRYPTO_SET_CIPHERSUITES
 
+# Check for openssl early data support
+TS_CHECK_EARLY_DATA
+
 saved_LIBS="$LIBS"
 TS_ADDTO([LIBS], ["$OPENSSL_LIBS"])
 
diff --git a/doc/admin-guide/files/records.config.en.rst b/doc/admin-guide/files/records.config.en.rst
index 0e8743a..9bef21f 100644
--- a/doc/admin-guide/files/records.config.en.rst
+++ b/doc/admin-guide/files/records.config.en.rst
@@ -3529,6 +3529,24 @@ Client-Related Configuration
    engines. This setting assumes an absolute path.  An example config file is at
    :ts:git:`contrib/openssl/load_engine.cnf`.
 
+TLS v1.3 0-RTT Configuration
+----------------------------
+
+.. note::
+   TLS v1.3 must be enabled in order to utilize 0-RTT early data.
+
+.. ts:cv:: CONFIG proxy.config.ssl.server.max_early_data INT 0
+
+   Specifies the maximum amount of early data in bytes that is permitted to be sent on a single connection.
+
+   The minimum value that enables early data, and the suggested value for this option are both 16384 (16KB).
+
+   Setting to ``0`` effectively disables 0-RTT.
+
+.. ts:cv:: CONFIG proxy.config.ssl.server.allow_early_data_params INT 0
+
+   Set to ``1`` to allow HTTP parameters on early data requests.
+
 OCSP Stapling Configuration
 ===========================
 
diff --git a/include/tscore/ink_config.h.in b/include/tscore/ink_config.h.in
index e994ea0..3ff2acb 100644
--- a/include/tscore/ink_config.h.in
+++ b/include/tscore/ink_config.h.in
@@ -78,6 +78,7 @@
 #define TS_USE_LINUX_NATIVE_AIO @use_linux_native_aio@
 #define TS_USE_REMOTE_UNWINDING @use_remote_unwinding@
 #define TS_USE_TLS_OCSP @use_tls_ocsp@
+#define TS_HAS_TLS_EARLY_DATA @has_tls_early_data@
 
 #define TS_HAS_SO_PEERCRED @has_so_peercred@
 
diff --git a/iocore/net/P_SSLConfig.h b/iocore/net/P_SSLConfig.h
index 54a43dd..628eb63 100644
--- a/iocore/net/P_SSLConfig.h
+++ b/iocore/net/P_SSLConfig.h
@@ -40,6 +40,8 @@
 #include "SSLSessionCache.h"
 #include "YamlSNIConfig.h"
 
+#include "P_SSLUtils.h"
+
 struct SSLCertLookup;
 struct ssl_ticket_key_block;
 
@@ -100,6 +102,10 @@ struct SSLConfigParams : public ConfigInfo {
   char *server_groups_list;
   char *client_groups_list;
 
+  static uint32_t server_max_early_data;
+  static uint32_t server_recv_max_early_data;
+  static bool server_allow_early_data_params;
+
   static int ssl_maxrecord;
   static bool ssl_allow_client_renegotiation;
 
diff --git a/iocore/net/P_SSLNetVConnection.h b/iocore/net/P_SSLNetVConnection.h
index 3b01405..609d6f1 100644
--- a/iocore/net/P_SSLNetVConnection.h
+++ b/iocore/net/P_SSLNetVConnection.h
@@ -409,6 +409,12 @@ public:
   bool protocol_mask_set = false;
   unsigned long protocol_mask;
 
+  // early data related stuff
+  bool early_data_finish            = false;
+  MIOBuffer *early_data_buf         = nullptr;
+  IOBufferReader *early_data_reader = nullptr;
+  int64_t read_from_early_data      = 0;
+
   // Only applies during the VERIFY certificate hooks (client and server side)
   // Means to give the plugin access to the data structure passed in during the underlying
   // openssl callback so the plugin can make more detailed decisions about the
diff --git a/iocore/net/SSLConfig.cc b/iocore/net/SSLConfig.cc
index a87eba3..99b3be2 100644
--- a/iocore/net/SSLConfig.cc
+++ b/iocore/net/SSLConfig.cc
@@ -41,7 +41,6 @@
 #include "HttpConfig.h"
 
 #include "P_Net.h"
-#include "P_SSLUtils.h"
 #include "P_SSLClientUtils.h"
 #include "P_SSLCertLookup.h"
 #include "SSLDiags.h"
@@ -66,6 +65,11 @@ init_ssl_ctx_func SSLConfigParams::init_ssl_ctx_cb          = nullptr;
 load_ssl_file_func SSLConfigParams::load_ssl_file_cb        = nullptr;
 IpMap *SSLConfigParams::proxy_protocol_ipmap                = nullptr;
 
+const uint32_t EARLY_DATA_DEFAULT_SIZE               = 16384;
+uint32_t SSLConfigParams::server_max_early_data      = 0;
+uint32_t SSLConfigParams::server_recv_max_early_data = EARLY_DATA_DEFAULT_SIZE;
+bool SSLConfigParams::server_allow_early_data_params = false;
+
 int SSLConfigParams::async_handshake_enabled = 0;
 char *SSLConfigParams::engine_conf_file      = nullptr;
 
@@ -278,6 +282,13 @@ SSLConfigParams::initialize()
   ssl_client_ctx_options |= SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION;
 #endif
 
+  REC_ReadConfigInteger(server_max_early_data, "proxy.config.ssl.server.max_early_data");
+  REC_ReadConfigInt32(server_allow_early_data_params, "proxy.config.ssl.server.allow_early_data_params");
+
+  // According to OpenSSL the default value is 16384,
+  // we keep it unless "server_max_early_data" is higher.
+  server_recv_max_early_data = std::max(server_max_early_data, EARLY_DATA_DEFAULT_SIZE);
+
   REC_ReadConfigStringAlloc(serverCertChainFilename, "proxy.config.ssl.server.cert_chain.filename");
   REC_ReadConfigStringAlloc(serverCertRelativePath, "proxy.config.ssl.server.cert.path");
   set_paths_helper(serverCertRelativePath, nullptr, &serverCertPathOnly, nullptr);
diff --git a/iocore/net/SSLNetVConnection.cc b/iocore/net/SSLNetVConnection.cc
index 051c97a..fa59bb4 100644
--- a/iocore/net/SSLNetVConnection.cc
+++ b/iocore/net/SSLNetVConnection.cc
@@ -34,6 +34,7 @@
 
 #include "P_Net.h"
 #include "P_SSLUtils.h"
+#include "P_SSLNextProtocolSet.h"
 #include "P_SSLConfig.h"
 #include "P_SSLClientUtils.h"
 #include "P_SSLSNI.h"
@@ -175,6 +176,38 @@ make_ssl_connection(SSL_CTX *ctx, SSLNetVConnection *netvc)
       BIO *wbio = BIO_new_fd(netvc->get_socket(), BIO_NOCLOSE);
       BIO_set_mem_eof_return(wbio, -1);
       SSL_set_bio(ssl, rbio, wbio);
+
+#if TS_HAS_TLS_EARLY_DATA
+      // Must disable OpenSSL's internal anti-replay if external cache is used with
+      // 0-rtt, otherwise session reuse will be broken. The freshness check described
+      // in https://tools.ietf.org/html/rfc8446#section-8.3 is still performed. But we
+      // still need to implement something to try to prevent replay atacks.
+      //
+      // We are now also disabling this when using OpenSSL's internal cache, since we
+      // are calling "ssl_accept" non-blocking, it seems to be confusing the anti-replay
+      // mechanism and causing session resumption to fail.
+      SSLConfig::scoped_config params;
+      if (SSL_version(ssl) >= TLS1_3_VERSION && params->server_max_early_data > 0) {
+        bool ret1 = false;
+        bool ret2 = false;
+        if ((ret1 = SSL_set_max_early_data(ssl, params->server_max_early_data)) == 1) {
+          Debug("ssl_early_data", "SSL_set_max_early_data: success");
+        } else {
+          Debug("ssl_early_data", "SSL_set_max_early_data: failed");
+        }
+
+        if ((ret2 = SSL_set_recv_max_early_data(ssl, params->server_recv_max_early_data)) == 1) {
+          Debug("ssl_early_data", "SSL_set_recv_max_early_data: success");
+        } else {
+          Debug("ssl_early_data", "SSL_set_recv_max_early_data: failed");
+        }
+
+        if (ret1 && ret2) {
+          Debug("ssl_early_data", "Must disable anti-replay if 0-rtt is enabled.");
+          SSL_set_options(ssl, SSL_OP_NO_ANTI_REPLAY);
+        }
+      }
+#endif
     }
 
     SSLNetVCAttach(ssl, netvc);
@@ -923,6 +956,17 @@ SSLNetVConnection::free(EThread *t)
 
   ats_free(tunnel_host);
 
+  if (early_data_reader != nullptr) {
+    early_data_reader->dealloc();
+  }
+
+  if (early_data_buf != nullptr) {
+    free_MIOBuffer(early_data_buf);
+  }
+
+  early_data_reader = nullptr;
+  early_data_buf    = nullptr;
+
   clear();
   SET_CONTINUATION_HANDLER(this, (SSLNetVConnHandler)&SSLNetVConnection::startEvent);
   ink_assert(con.fd == NO_FD);
diff --git a/iocore/net/SSLSessionTicket.cc b/iocore/net/SSLSessionTicket.cc
index 5610764..de7bce5 100644
--- a/iocore/net/SSLSessionTicket.cc
+++ b/iocore/net/SSLSessionTicket.cc
@@ -55,6 +55,7 @@ int
 ssl_callback_session_ticket(SSL *ssl, unsigned char *keyname, unsigned char *iv, EVP_CIPHER_CTX *cipher_ctx, HMAC_CTX *hctx,
                             int enc)
 {
+  SSLConfig::scoped_config config;
   SSLCertificateConfig::scoped_config lookup;
   SSLTicketKeyConfig::scoped_config params;
   SSLNetVConnection &netvc = *SSLNetVCAccess(ssl);
@@ -82,7 +83,7 @@ ssl_callback_session_ticket(SSL *ssl, unsigned char *keyname, unsigned char *iv,
     EVP_EncryptInit_ex(cipher_ctx, EVP_aes_128_cbc(), nullptr, most_recent_key.aes_key, iv);
     HMAC_Init_ex(hctx, most_recent_key.hmac_secret, sizeof(most_recent_key.hmac_secret), evp_md_func, nullptr);
 
-    Debug("ssl", "create ticket for a new session.");
+    Debug("ssl_session_ticket", "create ticket for a new session.");
     SSL_INCREMENT_DYN_STAT(ssl_total_tickets_created_stat);
     return 1;
   } else if (enc == 0) {
@@ -91,7 +92,7 @@ ssl_callback_session_ticket(SSL *ssl, unsigned char *keyname, unsigned char *iv,
         EVP_DecryptInit_ex(cipher_ctx, EVP_aes_128_cbc(), nullptr, keyblock->keys[i].aes_key, iv);
         HMAC_Init_ex(hctx, keyblock->keys[i].hmac_secret, sizeof(keyblock->keys[i].hmac_secret), evp_md_func, nullptr);
 
-        Debug("ssl", "verify the ticket for an existing session.");
+        Debug("ssl_session_ticket", "verify the ticket for an existing session.");
         // Increase the total number of decrypted tickets.
         SSL_INCREMENT_DYN_STAT(ssl_total_tickets_verified_stat);
 
@@ -100,12 +101,20 @@ ssl_callback_session_ticket(SSL *ssl, unsigned char *keyname, unsigned char *iv,
         }
 
         netvc.setSSLSessionCacheHit(true);
+
+#if TS_HAS_TLS_EARLY_DATA
+        if (SSL_version(ssl) >= TLS1_3_VERSION && config->server_max_early_data > 0) {
+          Debug("ssl_session_ticket", "make sure tickets are only used once.");
+          return 2;
+        }
+#endif
+
         // When we decrypt with an "older" key, encrypt the ticket again with the most recent key.
         return (i == 0) ? 1 : 2;
       }
     }
 
-    Debug("ssl", "keyname is not consistent.");
+    Debug("ssl_session_ticket", "keyname is not consistent.");
     SSL_INCREMENT_DYN_STAT(ssl_total_tickets_not_found_stat);
     return 0;
   }
diff --git a/iocore/net/SSLStats.cc b/iocore/net/SSLStats.cc
index a29adc3..f9d5304 100644
--- a/iocore/net/SSLStats.cc
+++ b/iocore/net/SSLStats.cc
@@ -173,7 +173,7 @@ SSLInitializeStatistics()
   RecRegisterRawStat(ssl_rsb, RECT_PROCESS, "proxy.process.ssl.ssl_session_cache_lock_contention", RECD_COUNTER, RECP_PERSISTENT,
                      (int)ssl_session_cache_lock_contention, RecRawStatSyncCount);
 
-  /* Track dynamic record size */
+  // Track dynamic record size
   RecRegisterRawStat(ssl_rsb, RECT_PROCESS, "proxy.process.ssl.default_record_size_count", RECD_COUNTER, RECP_PERSISTENT,
                      (int)ssl_total_dyn_def_tls_record_count, RecRawStatSyncSum);
   RecRegisterRawStat(ssl_rsb, RECT_PROCESS, "proxy.process.ssl.max_record_size_count", RECD_COUNTER, RECP_PERSISTENT,
@@ -181,7 +181,7 @@ SSLInitializeStatistics()
   RecRegisterRawStat(ssl_rsb, RECT_PROCESS, "proxy.process.ssl.redo_record_size_count", RECD_COUNTER, RECP_PERSISTENT,
                      (int)ssl_total_dyn_redo_tls_record_count, RecRawStatSyncCount);
 
-  /* error stats */
+  // error stats
   RecRegisterRawStat(ssl_rsb, RECT_PROCESS, "proxy.process.ssl.ssl_error_syscall", RECD_COUNTER, RECP_PERSISTENT,
                      (int)ssl_error_syscall, RecRawStatSyncCount);
   RecRegisterRawStat(ssl_rsb, RECT_PROCESS, "proxy.process.ssl.ssl_error_read_eos", RECD_COUNTER, RECP_PERSISTENT,
@@ -191,7 +191,7 @@ SSLInitializeStatistics()
   RecRegisterRawStat(ssl_rsb, RECT_PROCESS, "proxy.process.ssl.ssl_sni_name_set_failure", RECD_COUNTER, RECP_PERSISTENT,
                      (int)ssl_sni_name_set_failure, RecRawStatSyncCount);
 
-  /* ocsp stapling stats */
+  // ocsp stapling stats
   RecRegisterRawStat(ssl_rsb, RECT_PROCESS, "proxy.process.ssl.ssl_ocsp_revoked_cert_stat", RECD_COUNTER, RECP_PERSISTENT,
                      (int)ssl_ocsp_revoked_cert_stat, RecRawStatSyncCount);
   RecRegisterRawStat(ssl_rsb, RECT_PROCESS, "proxy.process.ssl.ssl_ocsp_unknown_cert_stat", RECD_COUNTER, RECP_PERSISTENT,
@@ -201,7 +201,7 @@ SSLInitializeStatistics()
   RecRegisterRawStat(ssl_rsb, RECT_PROCESS, "proxy.process.ssl.ssl_ocsp_refresh_cert_failure", RECD_INT, RECP_PERSISTENT,
                      (int)ssl_ocsp_refresh_cert_failure_stat, RecRawStatSyncCount);
 
-  /* SSL Version stats */
+  // SSL Version stats
   RecRegisterRawStat(ssl_rsb, RECT_PROCESS, "proxy.process.ssl.ssl_total_sslv3", RECD_COUNTER, RECP_PERSISTENT,
                      (int)ssl_total_sslv3, RecRawStatSyncCount);
   RecRegisterRawStat(ssl_rsb, RECT_PROCESS, "proxy.process.ssl.ssl_total_tlsv1", RECD_COUNTER, RECP_PERSISTENT,
@@ -213,6 +213,10 @@ SSLInitializeStatistics()
   RecRegisterRawStat(ssl_rsb, RECT_PROCESS, "proxy.process.ssl.ssl_total_tlsv13", RECD_COUNTER, RECP_PERSISTENT,
                      (int)ssl_total_tlsv13, RecRawStatSyncCount);
 
+  // TLSv1.3 0-RTT stats
+  RecRegisterRawStat(ssl_rsb, RECT_PROCESS, "proxy.process.ssl.early_data_received", RECD_INT, RECP_PERSISTENT,
+                     (int)ssl_early_data_received_count, RecRawStatSyncCount);
+
   // Get and register the SSL cipher stats. Note that we are using the default SSL context to obtain
   // the cipher list. This means that the set of ciphers is fixed by the build configuration and not
   // filtered by proxy.config.ssl.server.cipher_suite. This keeps the set of cipher suites stable across
diff --git a/iocore/net/SSLStats.h b/iocore/net/SSLStats.h
index ccd6e93..202aa15 100644
--- a/iocore/net/SSLStats.h
+++ b/iocore/net/SSLStats.h
@@ -84,6 +84,7 @@ enum SSL_Stats {
   ssl_session_cache_eviction,
   ssl_session_cache_lock_contention,
   ssl_session_cache_new_session,
+  ssl_early_data_received_count, // how many times we received early data
 
   /* error stats */
   ssl_error_syscall,
diff --git a/iocore/net/SSLUtils.cc b/iocore/net/SSLUtils.cc
index b1994c1..eb28242 100644
--- a/iocore/net/SSLUtils.cc
+++ b/iocore/net/SSLUtils.cc
@@ -1109,7 +1109,8 @@ SSLMultiCertConfigLoader::index_certificate(SSLCertLookup *lookup, SSLCertContex
 static void
 ssl_callback_info(const SSL *ssl, int where, int ret)
 {
-  Debug("ssl", "ssl_callback_info ssl: %p where: %d ret: %d State: %s", ssl, where, ret, SSL_state_string_long(ssl));
+  Debug("ssl", "ssl_callback_info ssl: %p, where: %d, ret: %d, State: %s", ssl, where, ret, SSL_state_string_long(ssl));
+
   SSLNetVConnection *netvc = SSLNetVCAccess(ssl);
 
   if ((where & SSL_CB_ACCEPT_LOOP) && netvc->getSSLHandShakeComplete() == true &&
@@ -1132,6 +1133,13 @@ ssl_callback_info(const SSL *ssl, int where, int ret)
 #endif
 #endif
 #endif
+#ifdef TLS1_3_VERSION
+      // TLSv1.3 has no renegotiation.
+      if (SSL_version(ssl) >= TLS1_3_VERSION) {
+        Debug("ssl", "TLSv1.3 has no renegotiation.");
+        return;
+      }
+#endif
       netvc->setSSLClientRenegotiationAbort(true);
       Debug("ssl", "ssl_callback_info trying to renegotiate from the client");
     }
@@ -1237,7 +1245,6 @@ SSLMultiCertConfigLoader::init_server_ssl_ctx(std::vector<X509 *> &cert_list, co
     SSL_CTX_sess_set_get_cb(ctx, ssl_get_cached_session);
 
     SSL_CTX_set_session_cache_mode(ctx, SSL_SESS_CACHE_SERVER | SSL_SESS_CACHE_NO_INTERNAL | additional_cache_flags);
-
     break;
   }
   }
@@ -1696,7 +1703,26 @@ SSLWriteBuffer(SSL *ssl, const void *buf, int64_t nbytes, int64_t &nwritten)
     return SSL_ERROR_NONE;
   }
   ERR_clear_error();
-  int ret = SSL_write(ssl, buf, static_cast<int>(nbytes));
+
+  int ret;
+#if TS_HAS_TLS_EARLY_DATA
+  if (SSL_version(ssl) >= TLS1_3_VERSION) {
+    if (SSL_is_init_finished(ssl)) {
+      ret = SSL_write(ssl, buf, static_cast<int>(nbytes));
+    } else {
+      size_t nwrite;
+      ret = SSL_write_early_data(ssl, buf, static_cast<size_t>(nbytes), &nwrite);
+      if (ret == 1) {
+        ret = nwrite;
+      }
+    }
+  } else {
+    ret = SSL_write(ssl, buf, static_cast<int>(nbytes));
+  }
+#else
+  ret = SSL_write(ssl, buf, static_cast<int>(nbytes));
+#endif
+
   if (ret > 0) {
     nwritten = ret;
     BIO *bio = SSL_get_wbio(ssl);
@@ -1724,6 +1750,63 @@ SSLReadBuffer(SSL *ssl, void *buf, int64_t nbytes, int64_t &nread)
     return SSL_ERROR_NONE;
   }
   ERR_clear_error();
+
+#if TS_HAS_TLS_EARLY_DATA
+  if (SSL_version(ssl) >= TLS1_3_VERSION) {
+    SSLNetVConnection *netvc = SSLNetVCAccess(ssl);
+
+    int64_t early_data_len = 0;
+    if (netvc->early_data_reader != nullptr) {
+      early_data_len = netvc->early_data_reader->read_avail();
+    }
+
+    if (early_data_len > 0) {
+      Debug("ssl_early_data", "Reading from early data buffer.");
+      netvc->read_from_early_data += netvc->early_data_reader->read(buf, nbytes < early_data_len ? nbytes : early_data_len);
+
+      if (nbytes < early_data_len) {
+        nread = nbytes;
+      } else {
+        nread = early_data_len;
+      }
+
+      return SSL_ERROR_NONE;
+    }
+
+    if (SSLConfigParams::server_max_early_data > 0 && !netvc->early_data_finish) {
+      Debug("ssl_early_data", "More early data to read.");
+      ssl_error_t ssl_error = SSL_ERROR_NONE;
+      size_t read_bytes     = 0;
+
+      int ret = SSL_read_early_data(ssl, buf, static_cast<size_t>(nbytes), &read_bytes);
+
+      if (ret == SSL_READ_EARLY_DATA_ERROR) {
+        Debug("ssl_early_data", "SSL_READ_EARLY_DATA_ERROR");
+        ssl_error = SSL_get_error(ssl, ret);
+        Debug("ssl_early_data", "Error reading early data: %s", ERR_error_string(ERR_get_error(), nullptr));
+      } else {
+        if ((nread = read_bytes) > 0) {
+          netvc->read_from_early_data += read_bytes;
+          SSL_INCREMENT_DYN_STAT(ssl_early_data_received_count);
+          if (is_debug_tag_set("ssl_early_data_show_received")) {
+            std::string early_data_str(reinterpret_cast<char *>(buf), nread);
+            Debug("ssl_early_data_show_received", "Early data buffer: \n%s", early_data_str.c_str());
+          }
+        }
+
+        if (ret == SSL_READ_EARLY_DATA_FINISH) {
+          netvc->early_data_finish = true;
+          Debug("ssl_early_data", "SSL_READ_EARLY_DATA_FINISH: size = %lu", nread);
+        } else {
+          Debug("ssl_early_data", "SSL_READ_EARLY_DATA_SUCCESS: size = %lu", nread);
+        }
+      }
+
+      return ssl_error;
+    }
+  }
+#endif
+
   int ret = SSL_read(ssl, buf, static_cast<int>(nbytes));
   if (ret > 0) {
     nread = ret;
@@ -1744,11 +1827,64 @@ ssl_error_t
 SSLAccept(SSL *ssl)
 {
   ERR_clear_error();
-  int ret = SSL_accept(ssl);
+
+  int ret       = 0;
+  int ssl_error = SSL_ERROR_NONE;
+
+#if TS_HAS_TLS_EARLY_DATA
+  SSLNetVConnection *netvc = SSLNetVCAccess(ssl);
+  if (SSLConfigParams::server_max_early_data > 0 && !netvc->early_data_finish) {
+    size_t nread;
+    if (netvc->early_data_buf == nullptr) {
+      netvc->early_data_buf    = new_MIOBuffer(BUFFER_SIZE_INDEX_16K);
+      netvc->early_data_reader = netvc->early_data_buf->alloc_reader();
+    }
+
+    while (true) {
+      IOBufferBlock *block = new_IOBufferBlock();
+      block->alloc(BUFFER_SIZE_INDEX_16K);
+      ret = SSL_read_early_data(ssl, block->buf(), index_to_buffer_size(BUFFER_SIZE_INDEX_16K), &nread);
+
+      if (ret == SSL_READ_EARLY_DATA_ERROR) {
+        Debug("ssl_early_data", "SSL_READ_EARLY_DATA_ERROR");
+        break;
+      } else {
+        if (nread > 0) {
+          block->fill(nread);
+          netvc->early_data_buf->append_block(block);
+          SSL_INCREMENT_DYN_STAT(ssl_early_data_received_count);
+
+          if (is_debug_tag_set("ssl_early_data_show_received")) {
+            std::string early_data_str(reinterpret_cast<char *>(block->buf()), nread);
+            Debug("ssl_early_data_show_received", "Early data buffer: \n%s", early_data_str.c_str());
+          }
+        }
+
+        if (ret == SSL_READ_EARLY_DATA_FINISH) {
+          netvc->early_data_finish = true;
+          Debug("ssl_early_data", "SSL_READ_EARLY_DATA_FINISH: size = %lu", nread);
+
+          if (netvc->early_data_reader->read_avail() == 0) {
+            Debug("ssl_early_data", "no data in early data buffer");
+            ERR_clear_error();
+            ret = SSL_accept(ssl);
+          }
+          break;
+        }
+        Debug("ssl_early_data", "SSL_READ_EARLY_DATA_SUCCESS: size = %lu", nread);
+      }
+    }
+  } else {
+    ret = SSL_accept(ssl);
+  }
+#else
+  ret = SSL_accept(ssl);
+#endif
+
   if (ret > 0) {
     return SSL_ERROR_NONE;
   }
-  int ssl_error = SSL_get_error(ssl, ret);
+  ssl_error = SSL_get_error(ssl, ret);
   if (ssl_error == SSL_ERROR_SSL && is_debug_tag_set("ssl.error.accept")) {
     char buf[512];
     unsigned long e = ERR_peek_last_error();
diff --git a/mgmt/RecordsConfig.cc b/mgmt/RecordsConfig.cc
index 6fa1c63..135e821 100644
--- a/mgmt/RecordsConfig.cc
+++ b/mgmt/RecordsConfig.cc
@@ -1150,7 +1150,10 @@ static const RecordElement RecordsConfig[] =
   ,
   {RECT_CONFIG, "proxy.config.ssl.client.groups_list", RECD_STRING, nullptr, RECU_RESTART_TS, RR_NULL, RECC_NULL, nullptr, RECA_NULL}
   ,
-
+  {RECT_CONFIG, "proxy.config.ssl.server.max_early_data", RECD_INT, "0", RECU_RESTART_TS, RR_NULL, RECC_NULL, nullptr, RECA_NULL}
+  ,
+  {RECT_CONFIG, "proxy.config.ssl.server.allow_early_data_params", RECD_INT, "0", RECU_RESTART_TS, RR_NULL, RECC_NULL, nullptr, RECA_NULL}
+  ,
   //##############################################################################
   //#
   //# OCSP (Online Certificate Status Protocol) Stapling Configuration
diff --git a/proxy/ProxyTransaction.cc b/proxy/ProxyTransaction.cc
index 73843e8..8042c34 100644
--- a/proxy/ProxyTransaction.cc
+++ b/proxy/ProxyTransaction.cc
@@ -30,7 +30,7 @@
 ProxyTransaction::ProxyTransaction() : VConnection(nullptr) {}
 
 void
-ProxyTransaction::new_transaction()
+ProxyTransaction::new_transaction(bool from_early_data)
 {
   ink_assert(_sm == nullptr);
 
@@ -39,7 +39,7 @@ ProxyTransaction::new_transaction()
 
   ink_release_assert(_proxy_ssn != nullptr);
   _sm = HttpSM::allocate();
-  _sm->init();
+  _sm->init(from_early_data);
   HttpTxnDebug("[%" PRId64 "] Starting transaction %d using sm [%" PRId64 "]", _proxy_ssn->connection_id(),
                _proxy_ssn->get_transact_count(), _sm->sm_id);
 
diff --git a/proxy/ProxyTransaction.h b/proxy/ProxyTransaction.h
index 60a9994..67defe3 100644
--- a/proxy/ProxyTransaction.h
+++ b/proxy/ProxyTransaction.h
@@ -37,7 +37,7 @@ public:
 
   /// Virtual Methods
   //
-  virtual void new_transaction();
+  virtual void new_transaction(bool from_early_data = false);
   virtual void attach_server_session(Http1ServerSession *ssession, bool transaction_done = true);
   Action *adjust_thread(Continuation *cont, int event, void *data);
   virtual void release(IOBufferReader *r);
diff --git a/proxy/hdrs/HTTP.h b/proxy/hdrs/HTTP.h
index c7c43e1..5acbaef 100644
--- a/proxy/hdrs/HTTP.h
+++ b/proxy/hdrs/HTTP.h
@@ -78,6 +78,7 @@ enum HTTPStatus {
   HTTP_STATUS_REQUEST_URI_TOO_LONG          = 414,
   HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE        = 415,
   HTTP_STATUS_RANGE_NOT_SATISFIABLE         = 416,
+  HTTP_STATUS_TOO_EARLY                     = 425,
 
   HTTP_STATUS_INTERNAL_SERVER_ERROR = 500,
   HTTP_STATUS_NOT_IMPLEMENTED       = 501,
@@ -506,6 +507,8 @@ public:
   /// also had a port, @c false otherwise.
   mutable bool m_port_in_header = false;
 
+  mutable bool early_data = false;
+
   HTTPHdr() = default; // Force the creation of the default constructor
 
   int valid() const;
@@ -629,6 +632,9 @@ public:
   const char *reason_get(int *length);
   void reason_set(const char *value, int length);
 
+  void mark_early_data(bool flag = true) const;
+  bool is_early_data() const;
+
   ParseResult parse_req(HTTPParser *parser, const char **start, const char *end, bool eof, bool strict_uri_parsing = false,
                         size_t max_request_line_size = UINT16_MAX, size_t max_hdr_field_size = 131070);
   ParseResult parse_resp(HTTPParser *parser, const char **start, const char *end, bool eof);
@@ -1221,6 +1227,26 @@ HTTPHdr::reason_set(const char *value, int length)
 /*-------------------------------------------------------------------------
   -------------------------------------------------------------------------*/
 
+inline void
+HTTPHdr::mark_early_data(bool flag) const
+{
+  ink_assert(valid());
+  early_data = flag;
+}
+
+/*-------------------------------------------------------------------------
+  -------------------------------------------------------------------------*/
+
+inline bool
+HTTPHdr::is_early_data() const
+{
+  ink_assert(valid());
+  return early_data;
+}
+
+/*-------------------------------------------------------------------------
+  -------------------------------------------------------------------------*/
+
 inline ParseResult
 HTTPHdr::parse_req(HTTPParser *parser, const char **start, const char *end, bool eof, bool strict_uri_parsing,
                    size_t max_request_line_size, size_t max_hdr_field_size)
diff --git a/proxy/hdrs/HdrToken.cc b/proxy/hdrs/HdrToken.cc
index a1ddec4..fcfc5c6 100644
--- a/proxy/hdrs/HdrToken.cc
+++ b/proxy/hdrs/HdrToken.cc
@@ -110,7 +110,10 @@ static const char *_hdrtoken_strs[] = {
   "X-ID", "X-Forwarded-For", "TE", "Strict-Transport-Security", "100-continue",
 
   // RFC-2739
-  "Forwarded"};
+  "Forwarded",
+
+  // RFC-8470
+  "Early-Data"};
 
 static HdrTokenTypeBinding _hdrtoken_strs_type_initializers[] = {
   {"file", HDRTOKEN_TYPE_SCHEME},
@@ -359,7 +362,10 @@ static const char *_hdrtoken_commonly_tokenized_strs[] = {
   "X-ID", "X-Forwarded-For", "TE", "Strict-Transport-Security", "100-continue",
 
   // RFC-2739
-  "Forwarded"};
+  "Forwarded",
+
+  // RFC-8470
+  "Early-Data"};
 
 /*-------------------------------------------------------------------------
   -------------------------------------------------------------------------*/
diff --git a/proxy/hdrs/MIME.cc b/proxy/hdrs/MIME.cc
index 7107401..75cab70 100644
--- a/proxy/hdrs/MIME.cc
+++ b/proxy/hdrs/MIME.cc
@@ -156,6 +156,7 @@ const char *MIME_FIELD_FORWARDED;
 const char *MIME_FIELD_SEC_WEBSOCKET_KEY;
 const char *MIME_FIELD_SEC_WEBSOCKET_VERSION;
 const char *MIME_FIELD_HTTP2_SETTINGS;
+const char *MIME_FIELD_EARLY_DATA;
 
 const char *MIME_VALUE_BYTES;
 const char *MIME_VALUE_CHUNKED;
@@ -272,6 +273,7 @@ int MIME_LEN_FORWARDED;
 int MIME_LEN_SEC_WEBSOCKET_KEY;
 int MIME_LEN_SEC_WEBSOCKET_VERSION;
 int MIME_LEN_HTTP2_SETTINGS;
+int MIME_LEN_EARLY_DATA;
 
 int MIME_WKSIDX_ACCEPT;
 int MIME_WKSIDX_ACCEPT_CHARSET;
@@ -351,6 +353,7 @@ int MIME_WKSIDX_FORWARDED;
 int MIME_WKSIDX_SEC_WEBSOCKET_KEY;
 int MIME_WKSIDX_SEC_WEBSOCKET_VERSION;
 int MIME_WKSIDX_HTTP2_SETTINGS;
+int MIME_WKSIDX_EARLY_DATA;
 
 /***********************************************************************
  *                                                                     *
@@ -745,11 +748,10 @@ mime_init()
     MIME_FIELD_X_ID                      = hdrtoken_string_to_wks("X-ID");
     MIME_FIELD_X_FORWARDED_FOR           = hdrtoken_string_to_wks("X-Forwarded-For");
     MIME_FIELD_FORWARDED                 = hdrtoken_string_to_wks("Forwarded");
-
-    MIME_FIELD_SEC_WEBSOCKET_KEY     = hdrtoken_string_to_wks("Sec-WebSocket-Key");
-    MIME_FIELD_SEC_WEBSOCKET_VERSION = hdrtoken_string_to_wks("Sec-WebSocket-Version");
-
-    MIME_FIELD_HTTP2_SETTINGS = hdrtoken_string_to_wks("HTTP2-Settings");
+    MIME_FIELD_SEC_WEBSOCKET_KEY         = hdrtoken_string_to_wks("Sec-WebSocket-Key");
+    MIME_FIELD_SEC_WEBSOCKET_VERSION     = hdrtoken_string_to_wks("Sec-WebSocket-Version");
+    MIME_FIELD_HTTP2_SETTINGS            = hdrtoken_string_to_wks("HTTP2-Settings");
+    MIME_FIELD_EARLY_DATA                = hdrtoken_string_to_wks("Early-Data");
 
     MIME_LEN_ACCEPT                    = hdrtoken_wks_to_length(MIME_FIELD_ACCEPT);
     MIME_LEN_ACCEPT_CHARSET            = hdrtoken_wks_to_length(MIME_FIELD_ACCEPT_CHARSET);
@@ -826,11 +828,10 @@ mime_init()
     MIME_LEN_X_ID                      = hdrtoken_wks_to_length(MIME_FIELD_X_ID);
     MIME_LEN_X_FORWARDED_FOR           = hdrtoken_wks_to_length(MIME_FIELD_X_FORWARDED_FOR);
     MIME_LEN_FORWARDED                 = hdrtoken_wks_to_length(MIME_FIELD_FORWARDED);
-
-    MIME_LEN_SEC_WEBSOCKET_KEY     = hdrtoken_wks_to_length(MIME_FIELD_SEC_WEBSOCKET_KEY);
-    MIME_LEN_SEC_WEBSOCKET_VERSION = hdrtoken_wks_to_length(MIME_FIELD_SEC_WEBSOCKET_VERSION);
-
-    MIME_LEN_HTTP2_SETTINGS = hdrtoken_wks_to_length(MIME_FIELD_HTTP2_SETTINGS);
+    MIME_LEN_SEC_WEBSOCKET_KEY         = hdrtoken_wks_to_length(MIME_FIELD_SEC_WEBSOCKET_KEY);
+    MIME_LEN_SEC_WEBSOCKET_VERSION     = hdrtoken_wks_to_length(MIME_FIELD_SEC_WEBSOCKET_VERSION);
+    MIME_LEN_HTTP2_SETTINGS            = hdrtoken_wks_to_length(MIME_FIELD_HTTP2_SETTINGS);
+    MIME_LEN_EARLY_DATA                = hdrtoken_wks_to_length(MIME_FIELD_EARLY_DATA);
 
     MIME_WKSIDX_ACCEPT                    = hdrtoken_wks_to_index(MIME_FIELD_ACCEPT);
     MIME_WKSIDX_ACCEPT_CHARSET            = hdrtoken_wks_to_index(MIME_FIELD_ACCEPT_CHARSET);
@@ -909,6 +910,7 @@ mime_init()
     MIME_WKSIDX_SEC_WEBSOCKET_KEY         = hdrtoken_wks_to_index(MIME_FIELD_SEC_WEBSOCKET_KEY);
     MIME_WKSIDX_SEC_WEBSOCKET_VERSION     = hdrtoken_wks_to_index(MIME_FIELD_SEC_WEBSOCKET_VERSION);
     MIME_WKSIDX_HTTP2_SETTINGS            = hdrtoken_wks_to_index(MIME_FIELD_HTTP2_SETTINGS);
+    MIME_WKSIDX_EARLY_DATA                = hdrtoken_wks_to_index(MIME_FIELD_EARLY_DATA);
 
     MIME_VALUE_BYTES                = hdrtoken_string_to_wks("bytes");
     MIME_VALUE_CHUNKED              = hdrtoken_string_to_wks("chunked");
diff --git a/proxy/hdrs/MIME.h b/proxy/hdrs/MIME.h
index 1752ad3..a1e9dbc 100644
--- a/proxy/hdrs/MIME.h
+++ b/proxy/hdrs/MIME.h
@@ -458,6 +458,7 @@ extern const char *MIME_FIELD_FORWARDED;
 extern const char *MIME_FIELD_SEC_WEBSOCKET_KEY;
 extern const char *MIME_FIELD_SEC_WEBSOCKET_VERSION;
 extern const char *MIME_FIELD_HTTP2_SETTINGS;
+extern const char *MIME_FIELD_EARLY_DATA;
 
 extern const char *MIME_VALUE_BYTES;
 extern const char *MIME_VALUE_CHUNKED;
@@ -559,7 +560,6 @@ extern int MIME_LEN_ATS_INTERNAL;
 extern int MIME_LEN_X_ID;
 extern int MIME_LEN_X_FORWARDED_FOR;
 extern int MIME_LEN_FORWARDED;
-
 extern int MIME_LEN_BYTES;
 extern int MIME_LEN_CHUNKED;
 extern int MIME_LEN_CLOSE;
@@ -582,11 +582,10 @@ extern int MIME_LEN_PROXY_REVALIDATE;
 extern int MIME_LEN_PUBLIC;
 extern int MIME_LEN_S_MAXAGE;
 extern int MIME_LEN_NEED_REVALIDATE_ONCE;
-
 extern int MIME_LEN_SEC_WEBSOCKET_KEY;
 extern int MIME_LEN_SEC_WEBSOCKET_VERSION;
-
 extern int MIME_LEN_HTTP2_SETTINGS;
+extern int MIME_LEN_EARLY_DATA;
 
 extern int MIME_WKSIDX_ACCEPT;
 extern int MIME_WKSIDX_ACCEPT_CHARSET;
@@ -664,6 +663,7 @@ extern int MIME_WKSIDX_X_ID;
 extern int MIME_WKSIDX_SEC_WEBSOCKET_KEY;
 extern int MIME_WKSIDX_SEC_WEBSOCKET_VERSION;
 extern int MIME_WKSIDX_HTTP2_SETTINGS;
+extern int MIME_WKSIDX_EARLY_DATA;
 
 /***********************************************************************
  *                                                                     *
diff --git a/proxy/http/Http1ClientSession.cc b/proxy/http/Http1ClientSession.cc
index d5b2269..38705df 100644
--- a/proxy/http/Http1ClientSession.cc
+++ b/proxy/http/Http1ClientSession.cc
@@ -139,6 +139,12 @@ Http1ClientSession::new_connection(NetVConnection *new_vc, MIOBuffer *iobuf, IOB
   ssn_start_time = Thread::get_hrtime();
   in_destroy     = false;
 
+  SSLNetVConnection *ssl_vc = dynamic_cast<SSLNetVConnection *>(new_vc);
+  if (ssl_vc != nullptr) {
+    read_from_early_data = ssl_vc->read_from_early_data;
+    Debug("ssl_early_data", "read_from_early_data = %ld", read_from_early_data);
+  }
+
   MUTEX_TRY_LOCK(lock, mutex, this_ethread());
   ink_assert(lock.is_locked());
 
@@ -460,7 +466,7 @@ Http1ClientSession::new_transaction()
   transact_count++;
 
   client_vc->add_to_active_queue();
-  trans.new_transaction();
+  trans.new_transaction(read_from_early_data > 0 ? true : false);
 }
 
 void
diff --git a/proxy/http/Http1ClientSession.h b/proxy/http/Http1ClientSession.h
index fd9afda..4fa61c6 100644
--- a/proxy/http/Http1ClientSession.h
+++ b/proxy/http/Http1ClientSession.h
@@ -124,6 +124,8 @@ private:
 
   int released_transactions = 0;
 
+  int64_t read_from_early_data = 0;
+
 public:
   // Link<Http1ClientSession> debug_link;
   LINK(Http1ClientSession, debug_link);
diff --git a/proxy/http/HttpSM.cc b/proxy/http/HttpSM.cc
index 7f36d63..540b94c 100644
--- a/proxy/http/HttpSM.cc
+++ b/proxy/http/HttpSM.cc
@@ -296,10 +296,12 @@ HttpSM::destroy()
 }
 
 void
-HttpSM::init()
+HttpSM::init(bool from_early_data)
 {
   milestones[TS_MILESTONE_SM_START] = Thread::get_hrtime();
 
+  _from_early_data = from_early_data;
+
   magic = HTTP_SM_MAGIC_ALIVE;
 
   // Unique state machine identifier
@@ -659,7 +661,7 @@ HttpSM::state_read_client_request_header(int event, void *data)
   }
 
   // We need to handle EOS as well as READ_READY because the client
-  // may have sent all of the data already followed by a fIN and that
+  // may have sent all of the data already followed by a FIN and that
   // should be OK.
   if (ua_raw_buffer_reader != nullptr) {
     bool do_blind_tunnel = false;
@@ -750,6 +752,24 @@ HttpSM::state_read_client_request_header(int event, void *data)
   case PARSE_RESULT_DONE:
     SMDebug("http", "[%" PRId64 "] done parsing client request header", sm_id);
 
+    if (_from_early_data) {
+      // Only allow early data for safe methods defined in RFC7231 Section 4.2.1.
+      // https://tools.ietf.org/html/rfc7231#section-4.2.1
+      SMDebug("ssl_early_data", "%d", t_state.hdr_info.client_request.method_get_wksidx());
+      if (!HttpTransactHeaders::is_method_safe(t_state.hdr_info.client_request.method_get_wksidx())) {
+        SMDebug("http", "client request was from early data but is NOT safe");
+        call_transact_and_set_next_state(HttpTransact::TooEarly);
+        return 0;
+      } else if (!SSLConfigParams::server_allow_early_data_params &&
+                 (t_state.hdr_info.client_request.m_http->u.req.m_url_impl->m_len_params > 0 ||
+                  t_state.hdr_info.client_request.m_http->u.req.m_url_impl->m_len_query > 0)) {
+        SMDebug("http", "client request was from early data but HAS parameters");
+        call_transact_and_set_next_state(HttpTransact::TooEarly);
+        return 0;
+      }
+      t_state.hdr_info.client_request.mark_early_data();
+    }
+
     ua_txn->set_session_active();
 
     if (t_state.hdr_info.client_request.version_get() == HTTPVersion(1, 1) &&
diff --git a/proxy/http/HttpSM.h b/proxy/http/HttpSM.h
index 1f237ef..e357267 100644
--- a/proxy/http/HttpSM.h
+++ b/proxy/http/HttpSM.h
@@ -213,7 +213,7 @@ public:
   HttpVCTableEntry *get_ua_entry();     // Added to get the ua_entry pointer  - YTS-TEAM
   HttpVCTableEntry *get_server_entry(); // Added to get the server_entry pointer
 
-  void init();
+  void init(bool from_early_data = false);
 
   void attach_client_session(ProxyTransaction *client_vc_arg, IOBufferReader *buffer_reader);
 
@@ -633,6 +633,7 @@ private:
   PostDataBuffers _postbuf;
   int _client_connection_id = -1, _client_transaction_id = -1;
   int _client_transaction_priority_weight = -1, _client_transaction_priority_dependence = -1;
+  bool _from_early_data = false;
 };
 
 // Function to get the cache_sm object - YTS Team, yamsat
diff --git a/proxy/http/HttpTransact.cc b/proxy/http/HttpTransact.cc
index c83c105..34f236e 100644
--- a/proxy/http/HttpTransact.cc
+++ b/proxy/http/HttpTransact.cc
@@ -571,6 +571,16 @@ HttpTransact::Forbidden(State *s)
 }
 
 void
+HttpTransact::TooEarly(State *s)
+{
+  TxnDebug("http_trans", "[TooEarly]"
+                         "Early Data method is not safe");
+  bootstrap_state_variables_from_request(s, &s->hdr_info.client_request);
+  build_error_response(s, HTTP_STATUS_TOO_EARLY, "Too Early", "too#early");
+  TRANSACT_RETURN(SM_ACTION_SEND_ERROR_CACHE_NOOP, nullptr);
+}
+
+void
 HttpTransact::HandleBlindTunnel(State *s)
 {
   URL u;
@@ -7571,6 +7581,10 @@ HttpTransact::build_request(State *s, HTTPHdr *base_request, HTTPHdr *outgoing_r
     TxnDebug("http_trans", "[build_request] request expect 100-continue headers removed");
   }
 
+  if (base_request->is_early_data()) {
+    outgoing_request->value_set_int(MIME_FIELD_EARLY_DATA, MIME_LEN_EARLY_DATA, 1);
+  }
+
   s->request_sent_time = ink_local_time();
   s->current.now       = s->request_sent_time;
 
diff --git a/proxy/http/HttpTransact.h b/proxy/http/HttpTransact.h
index 09ddc29..f181c61 100644
--- a/proxy/http/HttpTransact.h
+++ b/proxy/http/HttpTransact.h
@@ -930,6 +930,7 @@ public:
   static void HandleRequestAuthorized(State *s);
   static void BadRequest(State *s);
   static void Forbidden(State *s);
+  static void TooEarly(State *s);
   static void PostActiveTimeoutResponse(State *s);
   static void PostInactiveTimeoutResponse(State *s);
   static void DecideCacheLookup(State *s);
diff --git a/proxy/http2/Http2ClientSession.cc b/proxy/http2/Http2ClientSession.cc
index 981b5a9..d703925 100644
--- a/proxy/http2/Http2ClientSession.cc
+++ b/proxy/http2/Http2ClientSession.cc
@@ -199,6 +199,12 @@ Http2ClientSession::new_connection(NetVConnection *new_vc, MIOBuffer *iobuf, IOB
 
   this->connection_state.mutex = this->mutex;
 
+  SSLNetVConnection *ssl_vc = dynamic_cast<SSLNetVConnection *>(new_vc);
+  if (ssl_vc != nullptr) {
+    this->read_from_early_data = ssl_vc->read_from_early_data;
+    Debug("ssl_early_data", "read_from_early_data = %ld", this->read_from_early_data);
+  }
+
   Http2SsnDebug("session born, netvc %p", this->client_vc);
 
   this->client_vc->set_tcp_congestion_control(CLIENT_SIDE);
@@ -443,6 +449,11 @@ Http2ClientSession::state_read_connection_preface(int event, void *edata)
       return 0;
     }
 
+    // Check whether data is read from early data
+    if (this->read_from_early_data > 0) {
+      this->read_from_early_data -= this->read_from_early_data > nbytes ? nbytes : this->read_from_early_data;
+    }
+
     Http2SsnDebug("received connection preface");
     this->_reader->consume(nbytes);
     HTTP2_SET_SESSION_HANDLER(&Http2ClientSession::state_start_frame_read);
@@ -489,12 +500,19 @@ Http2ClientSession::do_start_frame_read(Http2ErrorCode &ret_error)
   Http2SsnDebug("receiving frame header");
   nbytes = copy_from_buffer_reader(buf, this->_reader, sizeof(buf));
 
+  this->cur_frame_from_early_data = false;
   if (!http2_parse_frame_header(make_iovec(buf), this->current_hdr)) {
     Http2SsnDebug("frame header parse failure");
     this->do_io_close();
     return -1;
   }
 
+  // Check whether data is read from early data
+  if (this->read_from_early_data > 0) {
+    this->read_from_early_data -= this->read_from_early_data > nbytes ? nbytes : this->read_from_early_data;
+    this->cur_frame_from_early_data = true;
+  }
+
   Http2SsnDebug("frame header length=%u, type=%u, flags=0x%x, streamid=%u", (unsigned)this->current_hdr.length,
                 (unsigned)this->current_hdr.type, (unsigned)this->current_hdr.flags, this->current_hdr.streamid);
 
@@ -552,8 +570,13 @@ Http2ClientSession::do_complete_frame_read()
   // XXX parse the frame and handle it ...
   ink_release_assert(this->_reader->read_avail() >= this->current_hdr.length);
 
-  Http2Frame frame(this->current_hdr, this->_reader);
+  Http2Frame frame(this->current_hdr, this->_reader, this->cur_frame_from_early_data);
   send_connection_event(&this->connection_state, HTTP2_SESSION_EVENT_RECV, &frame);
+  // Check whether data is read from early data
+  if (this->read_from_early_data > 0) {
+    this->read_from_early_data -=
+      this->read_from_early_data > this->current_hdr.length ? this->current_hdr.length : this->read_from_early_data;
+  }
   this->_reader->consume(this->current_hdr.length);
   ++(this->_n_frame_read);
 
diff --git a/proxy/http2/Http2ClientSession.h b/proxy/http2/Http2ClientSession.h
index eee6a3b..c3a7209 100644
--- a/proxy/http2/Http2ClientSession.h
+++ b/proxy/http2/Http2ClientSession.h
@@ -80,8 +80,11 @@ struct Http2UpgradeContext {
 class Http2Frame
 {
 public:
-  Http2Frame(const Http2FrameHeader &h, IOBufferReader *r) : hdr(h), ioreader(r) {}
-  Http2Frame(Http2FrameType type, Http2StreamId streamid, uint8_t flags) : hdr({0, (uint8_t)type, flags, streamid}) {}
+  Http2Frame(const Http2FrameHeader &h, IOBufferReader *r, bool e = false) : hdr(h), ioreader(r), from_early_data(e) {}
+  Http2Frame(Http2FrameType type, Http2StreamId streamid, uint8_t flags, bool e = false)
+    : hdr({0, (uint8_t)type, flags, streamid}), from_early_data(e)
+  {
+  }
 
   IOBufferReader *
   reader() const
@@ -147,6 +150,12 @@ public:
     }
   }
 
+  bool
+  is_from_early_data() const
+  {
+    return this->from_early_data;
+  }
+
   // noncopyable
   Http2Frame(Http2Frame &) = delete;
   Http2Frame &operator=(const Http2Frame &) = delete;
@@ -155,6 +164,7 @@ private:
   Http2FrameHeader hdr;       // frame header
   Ptr<IOBufferBlock> ioblock; // frame payload
   IOBufferReader *ioreader = nullptr;
+  bool from_early_data     = false;
 };
 
 class Http2ClientSession : public ProxySession
@@ -264,6 +274,9 @@ private:
 
   Event *_reenable_event = nullptr;
   int _n_frame_read      = 0;
+
+  int64_t read_from_early_data   = 0;
+  bool cur_frame_from_early_data = false;
 };
 
 extern ClassAllocator<Http2ClientSession> http2ClientSessionAllocator;
diff --git a/proxy/http2/Http2ConnectionState.cc b/proxy/http2/Http2ConnectionState.cc
index eb23143..fed9cd2 100644
--- a/proxy/http2/Http2ConnectionState.cc
+++ b/proxy/http2/Http2ConnectionState.cc
@@ -369,7 +369,7 @@ rcv_headers_frame(Http2ConnectionState &cstate, const Http2Frame &frame)
     if (!empty_request) {
       SCOPED_MUTEX_LOCK(stream_lock, stream->mutex, this_ethread());
       stream->mark_milestone(Http2StreamMilestone::START_TXN);
-      stream->new_transaction();
+      stream->new_transaction(frame.is_from_early_data());
       // Send request header to SM
       stream->send_request(cstate);
     }
@@ -925,7 +925,9 @@ rcv_continuation_frame(Http2ConnectionState &cstate, const Http2Frame &frame)
     // Set up the State Machine
     SCOPED_MUTEX_LOCK(stream_lock, stream->mutex, this_ethread());
     stream->mark_milestone(Http2StreamMilestone::START_TXN);
-    stream->new_transaction();
+    // This should be fine, need to verify whether we need to replace this with the
+    // "from_early_data" flag from the associated HEADERS frame.
+    stream->new_transaction(frame.is_from_early_data());
     // Send request header to SM
     stream->send_request(cstate);
   } else {
@@ -1020,6 +1022,25 @@ Http2ConnectionState::main_event_handler(int event, void *edata)
       break;
     }
 
+    // We need to be careful here, certain frame types are not safe over 0-rtt, tentative for now.
+    // DATA:          NO
+    // HEADERS:       YES (safe http methods only, can only be checked after parsing the payload).
+    // PRIORITY:      YES
+    // RST_STREAM:    NO
+    // SETTINGS:      YES
+    // PUSH_PROMISE:  NO
+    // PING:          YES
+    // GOAWAY:        NO
+    // WINDOW_UPDATE: YES
+    // CONTINUATION:  YES (safe http methods only, same as HEADERS frame).
+    if (frame->is_from_early_data() &&
+        (frame->header().type == HTTP2_FRAME_TYPE_DATA || frame->header().type == HTTP2_FRAME_TYPE_RST_STREAM ||
+         frame->header().type == HTTP2_FRAME_TYPE_PUSH_PROMISE || frame->header().type == HTTP2_FRAME_TYPE_GOAWAY)) {
+      Http2StreamDebug(ua_session, stream_id, "Discard a frame which is received from early data and has type=%x",
+                       frame->header().type);
+      break;
+    }
+
     if (frame_handlers[frame->header().type]) {
       error = frame_handlers[frame->header().type](*this, *frame);
     } else {
diff --git a/src/traffic_quic/traffic_quic.cc b/src/traffic_quic/traffic_quic.cc
index 1c290eb..69e2350 100644
--- a/src/traffic_quic/traffic_quic.cc
+++ b/src/traffic_quic/traffic_quic.cc
@@ -312,7 +312,7 @@ HttpSM::attach_client_session(ProxyTransaction *, IOBufferReader *)
 }
 
 void
-HttpSM::init()
+HttpSM::init(bool from_early_data)
 {
   ink_abort("do not call stub");
 }
diff --git a/tests/gold_tests/tls/early_h1_get.txt b/tests/gold_tests/tls/early_h1_get.txt
new file mode 100644
index 0000000..047f0a3
--- /dev/null
+++ b/tests/gold_tests/tls/early_h1_get.txt
@@ -0,0 +1,3 @@
+GET /early_get HTTP/1.1
+Host: 127.0.0.1
+
diff --git a/tests/gold_tests/tls/early_h1_post.txt b/tests/gold_tests/tls/early_h1_post.txt
new file mode 100644
index 0000000..e85fd17
--- /dev/null
+++ b/tests/gold_tests/tls/early_h1_post.txt
@@ -0,0 +1,6 @@
+POST /early_post HTTP/1.1
+Host: 127.0.0.1
+Content-Length: 11
+
+knock knock
+
diff --git a/tests/gold_tests/tls/early_h2_get.txt b/tests/gold_tests/tls/early_h2_get.txt
new file mode 100644
index 0000000..6f535e8
Binary files /dev/null and b/tests/gold_tests/tls/early_h2_get.txt differ
diff --git a/tests/gold_tests/tls/early_h2_multi1.txt b/tests/gold_tests/tls/early_h2_multi1.txt
new file mode 100644
index 0000000..71c3350
Binary files /dev/null and b/tests/gold_tests/tls/early_h2_multi1.txt differ
diff --git a/tests/gold_tests/tls/early_h2_multi2.txt b/tests/gold_tests/tls/early_h2_multi2.txt
new file mode 100644
index 0000000..cdd633a
Binary files /dev/null and b/tests/gold_tests/tls/early_h2_multi2.txt differ
diff --git a/tests/gold_tests/tls/early_h2_post.txt b/tests/gold_tests/tls/early_h2_post.txt
new file mode 100644
index 0000000..ecee5c7
Binary files /dev/null and b/tests/gold_tests/tls/early_h2_post.txt differ
diff --git a/tests/gold_tests/tls/h2_early_decode.py b/tests/gold_tests/tls/h2_early_decode.py
new file mode 100755
index 0000000..7f87527
--- /dev/null
+++ b/tests/gold_tests/tls/h2_early_decode.py
@@ -0,0 +1,257 @@
+#!/usr/bin/env python3
+
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+'''
+A simple tool to decode http2 frames for 0-rtt testing.
+'''
+
+import hpack
+import sys
+
+class Http2FrameDefs:
+
+    RESERVE_BIT_MASK = 0x7fffffff
+
+    DATA_FRAME = 0x00
+    HEADERS_FRAME = 0x01
+    PRIORITY_FRAME = 0x02
+    RST_STREAM_FRAME = 0x03
+    SETTINGS_FRAME = 0x04
+    PUSH_PROMISE_FRAME = 0x05
+    PING_FRAME = 0x06
+    GOAWAY_FRAME = 0x07
+    WINDOW_UPDATE_FRAME = 0x08
+    CONTINUATION_FRAME = 0x09
+
+    FRAME_TYPES = {
+        DATA_FRAME: 'DATA',
+        HEADERS_FRAME: 'HEADERS',
+        PRIORITY_FRAME: 'PRIORITY',
+        RST_STREAM_FRAME: 'RST_STREAM',
+        SETTINGS_FRAME: 'SETTINGS',
+        PUSH_PROMISE_FRAME: 'PUSH_PROMISE',
+        PING_FRAME: 'PING',
+        GOAWAY_FRAME: 'GOAWAY',
+        WINDOW_UPDATE_FRAME: 'WINDOW_UPDATE',
+        CONTINUATION_FRAME: 'CONTINUATION'
+    }
+
+    SETTINGS_HEADER_TABLE_SIZE = 0x01
+    SETTINGS_ENABLE_PUSH = 0x02
+    SETTINGS_MAX_CONCURRENT_STREAMS = 0x03
+    SETTINGS_INITIAL_WINDOW_SIZE = 0x04
+    SETTINGS_MAX_FRAME_SIZE = 0x05
+    SETTINGS_MAX_HEADER_LIST_SIZE = 0x06
+
+    SETTINGS_ID = {
+        SETTINGS_HEADER_TABLE_SIZE: 'HEADER_TABLE_SIZE',
+        SETTINGS_ENABLE_PUSH: 'ENABLE_PUSH',
+        SETTINGS_MAX_CONCURRENT_STREAMS: 'MAX_CONCURRENT_STREAMS',
+        SETTINGS_INITIAL_WINDOW_SIZE: 'INITIAL_WINDOW_SIZE',
+        SETTINGS_MAX_FRAME_SIZE: 'MAX_FRAME_SIZE',
+        SETTINGS_MAX_HEADER_LIST_SIZE: 'MAX_HEADER_LIST_SIZE'
+    }
+
+    RST_STREAM_NO_ERROR = 0x0
+    RST_STREAM_PROTOCOL_ERROR = 0x1
+    RST_STREAM_INTERNAL_ERROR = 0x2
+    RST_STREAM_FLOW_CONTROL_ERROR = 0x3
+    RST_STREAM_SETTINGS_TIMEOUT = 0x4
+    RST_STREAM_STREAM_CLOSED = 0x5
+    RST_STREAM_FRAME_SIZE_ERROR = 0x6
+    RST_STREAM_REFUSED_STREAM = 0x7
+    RST_STREAM_CANCEL = 0x8
+    RST_STREAM_COMPRESSION_ERROR = 0x9
+    RST_STREAM_CONNECT_ERROR = 0xa
+    RST_STREAM_ENHANCE_YOUR_CALM = 0xb
+    RST_STREAM_INADEQUATE_SECURITY = 0xc
+    RST_STREAM_HTTP_1_1_REQUIRED = 0xd
+
+    RST_STREAM_ERROR_CODES = {
+        RST_STREAM_NO_ERROR: 'NO_ERROR',
+        RST_STREAM_PROTOCOL_ERROR: 'PROTOCOL_ERROR',
+        RST_STREAM_INTERNAL_ERROR: 'INTERNAL_ERROR',
+        RST_STREAM_FLOW_CONTROL_ERROR: 'FLOW_CONTROL_ERROR',
+        RST_STREAM_SETTINGS_TIMEOUT: 'SETTINGS_TIMEOUT',
+        RST_STREAM_STREAM_CLOSED: 'STREAM_CLOSED',
+        RST_STREAM_FRAME_SIZE_ERROR: 'FRAME_SIZE_ERROR',
+        RST_STREAM_REFUSED_STREAM: 'REFUSED_STREAM',
+        RST_STREAM_CANCEL: 'CANCEL',
+        RST_STREAM_COMPRESSION_ERROR: 'COMPRESSION_ERROR',
+        RST_STREAM_CONNECT_ERROR: 'CONNECT_ERROR',
+        RST_STREAM_ENHANCE_YOUR_CALM: 'ENHANCE_YOUR_CALM',
+        RST_STREAM_INADEQUATE_SECURITY: 'INADEQUATE_SECURITY',
+        RST_STREAM_HTTP_1_1_REQUIRED: 'HTTP_1_1_REQUIRED'
+    }
+
+
+class Http2Frame:
+    def __init__(self, length, frame_type, flags, stream_id):
+        self.length = length
+        self.frame_type = frame_type
+        self.flags = flags
+        self.stream_id = stream_id
+        self.payload = None
+        self.decode_error = None
+        return
+
+    def add_payload(self, payload):
+        self.payload = payload
+        return
+
+    def read_data(self):
+        if self.frame_type == Http2FrameDefs.DATA_FRAME:
+            return '\n' + self.payload.decode('utf-8')
+        else:
+            return '\nError: Frame type mismatch: {0}'.format(Http2FrameDefs.FRAME_TYPES[self.frame_type])
+
+    def read_headers(self):
+        if self.frame_type == Http2FrameDefs.HEADERS_FRAME:
+            try:
+                decoder = hpack.Decoder()
+                decoded_data = decoder.decode(self.payload)
+                output_str = ''
+                for header in decoded_data:
+                    output_str += '\n'
+                    for each in header:
+                        output_str += each + ' '
+            except hpack.exceptions.InvalidTableIndex:
+                output_str = self.payload.hex()
+                output_str += '\nWarning: Decode failed: Invalid table index (not too important)'
+            return output_str
+        else:
+            return '\nError: Frame type mismatch: {0}'.format(Http2FrameDefs.FRAME_TYPES[self.frame_type])
+
+    def read_rst_stream(self):
+        if self.frame_type == Http2FrameDefs.RST_STREAM_FRAME:
+            error_code = int(self.payload.hex(), 16)
+            return '\nError Code = {0}'.format(Http2FrameDefs.RST_STREAM_ERROR_CODES[error_code])
+        else:
+            return '\nError: Frame type mismatch: {0}'.format(Http2FrameDefs.FRAME_TYPES[self.frame_type])
+
+    def read_settings(self):
+        if self.frame_type == Http2FrameDefs.SETTINGS_FRAME:
+            settings_str = ''
+            for i in range(0, self.length, 6):
+                settings_id = int(self.payload[i:i + 2].hex(), 16)
+                settings_val = int(self.payload[i + 2:i + 6].hex(), 16)
+                settings_str += '\n{0} = {1}'.format(Http2FrameDefs.SETTINGS_ID[settings_id], settings_val)
+            return settings_str
+        else:
+            return '\nError: Frame type mismatch: {0}'.format(Http2FrameDefs.FRAME_TYPES[self.frame_type])
+
+    def read_goaway(self):
+        if self.frame_type == Http2FrameDefs.GOAWAY_FRAME:
+            last_stream_id = int(self.payload[0:4].hex(), 16) & Http2FrameDefs.RESERVE_BIT_MASK
+            error_code = int(self.payload[4:8].hex(), 16)
+            debug_data = self.payload[8:].hex()
+            return '\nLast Stream ID = 0x{0:08x}\nError Code = 0x{1:08x}\nDebug Data = {2}'.format(
+                last_stream_id, error_code, debug_data
+            )
+        else:
+            return '\nError: Frame type mismatch: {0}'.format(Http2FrameDefs.FRAME_TYPES[self.frame_type])
+
+    def read_window_update(self):
+        if self.frame_type == Http2FrameDefs.WINDOW_UPDATE_FRAME:
+            window_size_increment = int(self.payload.hex(), 16) & Http2FrameDefs.RESERVE_BIT_MASK
+            return '\nWindow Size Increment = {0}'.format(window_size_increment)
+        else:
+            return '\nError: Frame type mismatch: {0}'.format(Http2FrameDefs.FRAME_TYPES[self.frame_type])
+
+    def print_payload(self):
+        if self.frame_type == Http2FrameDefs.DATA_FRAME:
+            return self.read_data()
+        elif self.frame_type == Http2FrameDefs.HEADERS_FRAME:
+            return self.read_headers()
+        elif self.frame_type == Http2FrameDefs.RST_STREAM_FRAME:
+            return self.read_rst_stream()
+        elif self.frame_type == Http2FrameDefs.SETTINGS_FRAME:
+            return self.read_settings()
+        elif self.frame_type == Http2FrameDefs.GOAWAY_FRAME:
+            return self.read_goaway()
+        elif self.frame_type == Http2FrameDefs.WINDOW_UPDATE_FRAME:
+            return self.read_window_update()
+        else:
+            return self.payload.hex()
+
+    def print(self):
+        output = 'Length: {0}\nType: {1}\nFlags: {2}\nStream ID: {3}\nPayload: {4}\n'.format(
+            self.length, Http2FrameDefs.FRAME_TYPES[self.frame_type], self.flags, self.stream_id, self.print_payload()
+        )
+        if self.decode_error is not None:
+            output += self.decode_error + '\n'
+        return output
+
+    def __str__(self):
+        return self.print()
+
+class Decoder:
+    def read_frame_header(self, data):
+        frame = Http2Frame(
+            length=int(data[0:3].hex(), 16),
+            frame_type=int(data[3:4].hex(), 16),
+            flags=int(data[4:5].hex(), 16),
+            stream_id=int(data[5:9].hex(), 16) & Http2FrameDefs.RESERVE_BIT_MASK
+        )
+        return frame
+
+    def decode(self, data):
+        temp_data = data
+        frames = []
+        while len(temp_data) >= 9:
+            frame_header = temp_data[0:9]
+            frame = self.read_frame_header(frame_header)
+            if frame.length > len(temp_data[9:]):
+                frame.decode_error = 'Error: Payload length greater than data: {0} > {1}'.format(frame.length, len(temp_data[9:]))
+                frame.add_payload(temp_data[9:])
+                frames.append(frame)
+            else:
+                frame.add_payload(temp_data[9:9 + frame.length])
+                frames.append(frame)
+                temp_data = temp_data[9 + frame.length:]
+        return frames
+
+def main():
+    # input file is output from openssl s_client.
+    # sample command to get this output:
+    # openssl s_client -bind 127.0.0.1:61991 -connect 127.0.0.1:61992 -tls1_3 -quiet -sess_out /home/duke/Dev/ats-test/sess.dat -sess_in /home/duke/Dev/ats-test/sess.dat -early_data ./gold_tests/tls/early2.txt >! _sandbox/tls_0rtt_server/early2_out.txt 2>&1
+
+    if len(sys.argv) < 2:
+        print('Error: No input file to decode.')
+        exit(1)
+
+    lines = None
+    with open(sys.argv[1], 'rb') as in_file:
+        lines = in_file.readlines()
+
+    data = b''
+    for line in lines:
+        if line.startswith(bytes('SSL_connect:', 'utf-8')) or \
+            line.startswith(bytes('SSL3 alert', 'utf-8')) or \
+            bytes('Can\'t use SSL_get_servername', 'utf-8') in line:
+            continue
+        data += line
+
+    d = Decoder()
+    frames = d.decode(data)
+    for frame in frames:
+        print(frame)
+    exit(0)
+
+if __name__ == "__main__":
+    main()
diff --git a/tests/gold_tests/tls/h2_early_gen.py b/tests/gold_tests/tls/h2_early_gen.py
new file mode 100755
index 0000000..37e63c2
--- /dev/null
+++ b/tests/gold_tests/tls/h2_early_gen.py
@@ -0,0 +1,185 @@
+#!/usr/bin/env python3
+
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+'''
+A simple tool to generate some raw http2 frames for 0-rtt testing.
+'''
+
+# http2 frame format:
+# +-----------------------------------------------+
+# |                 Length (24)                   |
+# +---------------+---------------+---------------+
+# |   Type (8)    |   Flags (8)   |
+# +-+-------------+---------------+-------------------------------+
+# |R|                 Stream Identifier (31)                      |
+# +=+=============================================================+
+# |                   Frame Payload (0...)                      ...
+# +---------------------------------------------------------------+
+
+import hpack
+import os
+import sys
+
+H2_PREFACE = bytes.fromhex('505249202a20485454502f322e300d0a0d0a534d0d0a0d0a')
+
+RESERVED_BIT_MASK = 0x7FFFFFFF
+
+TYPE_HEADERS_FRAME = 0x01
+TYPE_SETTINGS_FRAME = 0x04
+TYPE_WINDOW_UPDATE_FRAME = 0x08
+
+SETTINGS_HEADER_TABLE_SIZE = 0x01
+SETTINGS_ENABLE_PUSH = 0x02
+SETTINGS_MAX_CONCURRENT_STREAMS = 0x03
+SETTINGS_INITIAL_WINDOW_SIZE = 0x04
+SETTINGS_MAX_FRAME_SIZE = 0x05
+SETTINGS_MAX_HEADER_LIST_SIZE = 0x06
+
+HEADERS_FLAG_END_STREAM = 0x01
+HEADERS_FLAG_END_HEADERS = 0x04
+HEADERS_FLAG_END_PADDED = 0x08
+HEADERS_FLAG_END_PRIORITY = 0x20
+
+CURRENT_SCRIPT_PATH = os.path.dirname(os.path.abspath(__file__))
+
+def encode_payload(data):
+    encoder = hpack.Encoder()
+    data_encoded = encoder.encode(data)
+    return data_encoded
+
+def make_frame(frame_length, frame_type, frame_flags, frame_stream_id, frame_payload):
+    frame_length = bytes.fromhex('{0:06x}'.format(frame_length))
+    frame_type = bytes.fromhex('{0:02x}'.format(frame_type))
+    frame_flags = bytes.fromhex('{0:02x}'.format(frame_flags))
+    frame_stream_id = bytes.fromhex('{0:08x}'.format(RESERVED_BIT_MASK & frame_stream_id))
+
+    frame = frame_length + frame_type + frame_flags + frame_stream_id
+
+    if frame_payload is not None:
+        frame += frame_payload
+
+    return frame
+
+def make_settins_frame(ack=False, empty=False):
+    payload = ''
+    if not ack and not empty:
+        payload += '{0:04x}{1:08x}'.format(SETTINGS_ENABLE_PUSH, 0)
+        payload += '{0:04x}{1:08x}'.format(SETTINGS_MAX_CONCURRENT_STREAMS, 100)
+        payload += '{0:04x}{1:08x}'.format(SETTINGS_INITIAL_WINDOW_SIZE, 1073741824)
+    payload = bytes.fromhex(payload)
+
+    frame = make_frame(
+        frame_length = len(payload),
+        frame_type = TYPE_SETTINGS_FRAME,
+        frame_flags = 1 if ack else 0,
+        frame_stream_id = 0,
+        frame_payload = payload
+    )
+
+    return frame
+
+def make_window_update_frame():
+    payload = '{0:08x}'.format(RESERVED_BIT_MASK & 1073676289)
+    payload = bytes.fromhex(payload)
+
+    frame = make_frame(
+        frame_length = len(payload),
+        frame_type = TYPE_WINDOW_UPDATE_FRAME,
+        frame_flags = 0,
+        frame_stream_id = 0,
+        frame_payload = payload
+    )
+    return frame
+
+def make_headers_frame(method, path='', stream_id=0x01):
+    headers = []
+    if method == 'get':
+        headers.append((':method', 'GET'))
+        if path != '':
+            headers.append((':path', path))
+        else:
+            headers.append((':path', '/early_get'))
+    elif method == 'post':
+        headers.append((':method', 'POST'))
+        if path != '':
+            headers.append((':path', path))
+        else:
+            headers.append((':path', '/early_post'))
+
+    headers.extend([
+        (':scheme', 'http'),
+        (':authority', '127.0.0.1'),
+        ('host', '127.0.0.1'),
+        ('accept', '*/*')
+    ])
+
+    headers_encoded = encode_payload(headers)
+
+    frame = make_frame(
+        frame_length = len(headers_encoded),
+        frame_type = TYPE_HEADERS_FRAME,
+        frame_flags = HEADERS_FLAG_END_STREAM | HEADERS_FLAG_END_HEADERS,
+        frame_stream_id = stream_id,
+        frame_payload = headers_encoded
+    )
+
+    return frame
+
+def make_h2_req(test):
+    h2_req = H2_PREFACE
+    if test == 'get' or test == 'post':
+        frames = [
+            make_settins_frame(ack=True),
+            make_headers_frame(test)
+        ]
+        for frame in frames:
+            h2_req += frame
+    elif test == 'multi1':
+        frames = [
+            make_settins_frame(ack=True),
+            make_headers_frame('get', '/early_multi_1', 1),
+            make_headers_frame('get', '/early_multi_2', 3),
+            make_headers_frame('get', '/early_multi_3', 5)
+        ]
+        for frame in frames:
+            h2_req += frame
+    elif test == 'multi2':
+        frames = [
+            make_settins_frame(ack=True),
+            make_headers_frame('get', '/early_multi_1', 1),
+            make_headers_frame('post', stream_id=3),
+            make_headers_frame('get', '/early_multi_3', 5)
+        ]
+        for frame in frames:
+            h2_req += frame
+    else:
+        pass
+    return h2_req
+
+def write_to_file(data, file_name):
+    with open(file_name, 'wb') as out_file:
+        out_file.write(data)
+    return
+
+def main():
+    test = sys.argv[1]
+    write_to_file(make_h2_req(test), os.path.join(CURRENT_SCRIPT_PATH, 'early_h2_{0}.txt'.format(test)))
+    exit(0)
+
+if __name__ == '__main__':
+    main()
diff --git a/tests/gold_tests/tls/test-0rtt-s_client.py b/tests/gold_tests/tls/test-0rtt-s_client.py
new file mode 100644
index 0000000..05c7165
--- /dev/null
+++ b/tests/gold_tests/tls/test-0rtt-s_client.py
@@ -0,0 +1,69 @@
+'''
+'''
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+import subprocess
+import sys
+import os
+import shlex
+import h2_early_decode
+
+def main():
+    ats_port = sys.argv[1]
+    http_ver = sys.argv[2]
+    test = sys.argv[3]
+    sess_file_path = os.path.join(sys.argv[4], 'sess.dat')
+    early_data_file_path = os.path.join(sys.argv[4], 'early_{0}_{1}.txt'.format(http_ver, test))
+
+    s_client_cmd_1 = shlex.split('openssl s_client -connect 127.0.0.1:{0} -tls1_3 -quiet -sess_out {1}'.format(ats_port, sess_file_path))
+    s_client_cmd_2 = shlex.split('openssl s_client -connect 127.0.0.1:{0} -tls1_3 -quiet -sess_in {1} -early_data {2}'.format(ats_port, sess_file_path, early_data_file_path))
+
+    create_sess_proc = subprocess.Popen(s_client_cmd_1, env=os.environ.copy(), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+    try:
+        output = create_sess_proc.communicate(timeout=1)[0]
+    except subprocess.TimeoutExpired:
+        create_sess_proc.kill()
+        output = create_sess_proc.communicate()[0]
+
+    reuse_sess_proc = subprocess.Popen(s_client_cmd_2, env=os.environ.copy(), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+    try:
+        output = reuse_sess_proc.communicate(timeout=1)[0]
+    except subprocess.TimeoutExpired:
+        reuse_sess_proc.kill()
+        output = reuse_sess_proc.communicate()[0]
+
+    if http_ver == 'h2':
+        lines = output.split(bytes('\n', 'utf-8'))
+        data = b''
+        for line in lines:
+            line += b'\n'
+            if line.startswith(bytes('SSL_connect:', 'utf-8')) or \
+                line.startswith(bytes('SSL3 alert', 'utf-8')) or \
+                bytes('Can\'t use SSL_get_servername', 'utf-8') in line:
+                continue
+            data += line
+        d = h2_early_decode.Decoder()
+        frames = d.decode(data)
+        for frame in frames:
+            print(frame)
+    else:
+        print(output.decode('utf-8'))
+
+    exit(0)
+
+if __name__ == '__main__':
+    main()
diff --git a/tests/gold_tests/tls/tls_0rtt_server.test.py b/tests/gold_tests/tls/tls_0rtt_server.test.py
new file mode 100644
index 0000000..e438846
--- /dev/null
+++ b/tests/gold_tests/tls/tls_0rtt_server.test.py
@@ -0,0 +1,193 @@
+'''
+'''
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+Test.Summary = '''
+Test ATS TLSv1.3 0-RTT support
+'''
+
+Test.SkipUnless(Condition.HasOpenSSLVersion('1.1.1'))
+
+ts = Test.MakeATSProcess('ts', select_ports=True, enable_tls=True)
+server = Test.MakeOriginServer('server')
+
+request_header1 = {
+    'headers': 'GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n',
+    'timestamp': '1469733493.993',
+    'body': ''
+}
+response_header1 = {
+    'headers': 'HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n',
+    'timestamp': '1469733493.993',
+    'body': 'curl test'
+}
+request_header2 = {
+    'headers': 'GET /early_get HTTP/1.1\r\nHost: www.example.com\r\n\r\n',
+    'timestamp': '1469733493.993',
+    'body': ''
+}
+response_header2 = {
+    'headers': 'HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n',
+    'timestamp': '1469733493.993',
+    'body': 'early data accepted'
+}
+request_header3 = {
+    'headers': 'POST /early_post HTTP/1.1\r\nHost: www.example.com\r\nContent-Length: 11\r\n\r\n',
+    'timestamp': '1469733493.993',
+    'body': 'knock knock'
+}
+response_header3 = {
+    'headers': 'HTTP/1.1 200 OK\r\nServer: uServer\r\nConnection: close\r\nTransfer-Encoding: chunked\r\n\r\n',
+    'timestamp': '1415926535.898',
+    'body': ''
+}
+request_header4 = {
+    'headers': 'GET /early_multi_1 HTTP/1.1\r\nHost: www.example.com\r\n\r\n',
+    'timestamp': '1469733493.993',
+    'body': ''
+}
+response_header4 = {
+    'headers': 'HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n',
+    'timestamp': '1469733493.993',
+    'body': 'early data accepted multi_1'
+}
+request_header5 = {
+    'headers': 'GET /early_multi_2 HTTP/1.1\r\nHost: www.example.com\r\n\r\n',
+    'timestamp': '1469733493.993',
+    'body': ''
+}
+response_header5 = {
+    'headers': 'HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n',
+    'timestamp': '1469733493.993',
+    'body': 'early data accepted multi_2'
+}
+request_header6 = {
+    'headers': 'GET /early_multi_3 HTTP/1.1\r\nHost: www.example.com\r\n\r\n',
+    'timestamp': '1469733493.993',
+    'body': ''
+}
+response_header6 = {
+    'headers': 'HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n',
+    'timestamp': '1469733493.993',
+    'body': 'early data accepted multi_3'
+}
+server.addResponse('sessionlog.json', request_header1, response_header1)
+server.addResponse('sessionlog.json', request_header2, response_header2)
+server.addResponse('sessionlog.json', request_header3, response_header3)
+server.addResponse('sessionlog.json', request_header4, response_header4)
+server.addResponse('sessionlog.json', request_header5, response_header5)
+server.addResponse('sessionlog.json', request_header6, response_header6)
+
+ts.addSSLfile('ssl/server.pem')
+ts.addSSLfile('ssl/server.key')
+
+ts.Setup.Copy('test-0rtt-s_client.py')
+ts.Setup.Copy('h2_early_decode.py')
+ts.Setup.Copy('early_h1_get.txt')
+ts.Setup.Copy('early_h1_post.txt')
+ts.Setup.Copy('early_h2_get.txt')
+ts.Setup.Copy('early_h2_post.txt')
+ts.Setup.Copy('early_h2_multi1.txt')
+ts.Setup.Copy('early_h2_multi2.txt')
+
+ts.Disk.records_config.update({
+    'proxy.config.diags.debug.enabled': 1,
+    'proxy.config.diags.debug.tags': 'http',
+    'proxy.config.exec_thread.autoconfig': 0,
+    'proxy.config.exec_thread.limit': 8,
+    'proxy.config.http.server_ports': '{0}:proto=http2;http:ssl'.format(ts.Variables.ssl_port),
+    'proxy.config.ssl.server.cert.path': '{0}'.format(ts.Variables.SSLDir),
+    'proxy.config.ssl.server.private_key.path': '{0}'.format(ts.Variables.SSLDir),
+    'proxy.config.ssl.session_cache': 2,
+    'proxy.config.ssl.session_cache.size': 512000,
+    'proxy.config.ssl.session_cache.timeout': 7200,
+    'proxy.config.ssl.session_cache.num_buckets': 32768,
+    'proxy.config.ssl.server.session_ticket.enable': 1,
+    'proxy.config.ssl.server.max_early_data': 16384,
+    'proxy.config.ssl.server.allow_early_data_params': 0,
+    'proxy.config.ssl.server.cipher_suite': 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-DSS-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SH [...]
+})
+
+ts.Disk.ssl_multicert_config.AddLine(
+    'dest_ip=* ssl_cert_name=server.pem ssl_key_name=server.key'
+)
+
+ts.Disk.remap_config.AddLine(
+    'map / http://127.0.0.1:{0}'.format(server.Variables.Port)
+)
+
+tr = Test.AddTestRun('Basic Curl Test')
+tr.Processes.Default.Command = 'curl https://127.0.0.1:{0} -k'.format(ts.Variables.ssl_port)
+tr.Processes.Default.ReturnCode = 0
+tr.Processes.Default.StartBefore(server)
+tr.Processes.Default.StartBefore(Test.Processes.ts, ready=When.PortOpen(ts.Variables.ssl_port))
+tr.Processes.Default.Streams.All = Testers.ContainsExpression('curl test', 'Making sure the basics still work')
+tr.Processes.Default.Streams.All += Testers.ExcludesExpression('early data accepted', '')
+tr.StillRunningAfter = server
+tr.StillRunningAfter += ts
+
+tr = Test.AddTestRun('TLSv1.3 0-RTT Support (HTTP/1.1 GET)')
+tr.Processes.Default.Command = 'python3 test-0rtt-s_client.py {0} {1} {2} {3}'.format(ts.Variables.ssl_port, 'h1', 'get', Test.RunDirectory)
+tr.Processes.Default.ReturnCode = 0
+tr.Processes.Default.Streams.All = Testers.ContainsExpression('early data accepted', '')
+tr.Processes.Default.Streams.All += Testers.ExcludesExpression('curl test', '')
+tr.StillRunningAfter = server
+tr.StillRunningAfter += ts
+
+tr = Test.AddTestRun('TLSv1.3 0-RTT Support (HTTP/1.1 POST)')
+tr.Processes.Default.Command = 'python3 test-0rtt-s_client.py {0} {1} {2} {3}'.format(ts.Variables.ssl_port, 'h1', 'post', Test.RunDirectory)
+tr.Processes.Default.ReturnCode = 0
+tr.Processes.Default.Streams.All = Testers.ContainsExpression('HTTP/1.1 425 Too Early', '')
+tr.Processes.Default.Streams.All += Testers.ExcludesExpression('curl test', '')
+tr.Processes.Default.Streams.All += Testers.ExcludesExpression('early data accepted', '')
+tr.StillRunningAfter = server
+tr.StillRunningAfter += ts
+
+tr = Test.AddTestRun('TLSv1.3 0-RTT Support (HTTP/2 GET)')
+tr.Processes.Default.Command = 'python3 test-0rtt-s_client.py {0} {1} {2} {3}'.format(ts.Variables.ssl_port, 'h2', 'get', Test.RunDirectory)
+tr.Processes.Default.ReturnCode = 0
+tr.Processes.Default.Streams.All = Testers.ContainsExpression('early data accepted', '')
+tr.Processes.Default.Streams.All += Testers.ExcludesExpression('curl test', '')
+tr.StillRunningAfter = server
+tr.StillRunningAfter += ts
+
+tr = Test.AddTestRun('TLSv1.3 0-RTT Support (HTTP/2 POST)')
+tr.Processes.Default.Command = 'python3 test-0rtt-s_client.py {0} {1} {2} {3}'.format(ts.Variables.ssl_port, 'h2', 'post', Test.RunDirectory)
+tr.Processes.Default.ReturnCode = 0
+tr.Processes.Default.Streams.All = Testers.ContainsExpression(':status 425', 'Only safe methods are allowed')
+tr.Processes.Default.Streams.All += Testers.ExcludesExpression('curl test', '')
+tr.Processes.Default.Streams.All += Testers.ExcludesExpression('early data accepted', '')
+tr.StillRunningAfter = server
+tr.StillRunningAfter += ts
+
+tr = Test.AddTestRun('TLSv1.3 0-RTT Support (HTTP/2 Multiplex)')
+tr.Processes.Default.Command = 'python3 test-0rtt-s_client.py {0} {1} {2} {3}'.format(ts.Variables.ssl_port, 'h2', 'multi1', Test.RunDirectory)
+tr.Processes.Default.ReturnCode = 0
+tr.Processes.Default.Streams.All = Testers.ContainsExpression('early data accepted multi_1', '')
+tr.Processes.Default.Streams.All += Testers.ContainsExpression('early data accepted multi_2', '')
+tr.Processes.Default.Streams.All += Testers.ContainsExpression('early data accepted multi_3', '')
+tr.Processes.Default.Streams.All += Testers.ExcludesExpression('curl test', '')
+tr.StillRunningAfter = server
+tr.StillRunningAfter += ts
+
+tr = Test.AddTestRun('TLSv1.3 0-RTT Support (HTTP/2 Multiplex with POST)')
+tr.Processes.Default.Command = 'python3 test-0rtt-s_client.py {0} {1} {2} {3}'.format(ts.Variables.ssl_port, 'h2', 'multi2', Test.RunDirectory)
+tr.Processes.Default.ReturnCode = 0
+tr.Processes.Default.Streams.All = Testers.ContainsExpression('early data accepted multi_1', '')
+tr.Processes.Default.Streams.All += Testers.ContainsExpression(':status 425', 'Only safe methods are allowed')
+tr.Processes.Default.Streams.All += Testers.ContainsExpression('early data accepted multi_3', '')
+tr.Processes.Default.Streams.All += Testers.ExcludesExpression('curl test', '')