diff --git a/build/crypto.m4 b/build/crypto.m4 index 6361955abbe..64787b4a1dc 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 d479c69f25c..634d17ec776 100644 --- a/configure.ac +++ b/configure.ac @@ -1277,6 +1277,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 f0177e5b1ee..16a6f6cb7f9 100644 --- a/doc/admin-guide/files/records.config.en.rst +++ b/doc/admin-guide/files/records.config.en.rst @@ -3497,6 +3497,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 e994ea0a38c..3ff2acbcf2d 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 54a43dd2b4d..628eb63fde5 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 3b014050777..609d6f1f92f 100644 --- a/iocore/net/P_SSLNetVConnection.h +++ b/iocore/net/P_SSLNetVConnection.h @@ -409,6 +409,12 @@ class SSLNetVConnection : public UnixNetVConnection, public ALPNSupport 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 a87eba3df84..99b3be27187 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 a29f1b241dc..a5c7797ba8a 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 5610764fc30..de7bce5527b 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 9ab09eda41a..0a3951372a3 100644 --- a/iocore/net/SSLStats.cc +++ b/iocore/net/SSLStats.cc @@ -169,7 +169,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, @@ -177,7 +177,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, @@ -187,7 +187,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, @@ -197,7 +197,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, @@ -209,6 +209,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 2ad0ce0b87e..cf1e24f80ac 100644 --- a/iocore/net/SSLStats.h +++ b/iocore/net/SSLStats.h @@ -83,6 +83,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 9e8b352abbc..752dac88d87 100644 --- a/iocore/net/SSLUtils.cc +++ b/iocore/net/SSLUtils.cc @@ -1108,7 +1108,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 && @@ -1130,6 +1131,13 @@ ssl_callback_info(const SSL *ssl, int where, int ret) if (state == TLS_ST_SR_CLNT_HELLO) { #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"); @@ -1236,7 +1244,6 @@ SSLMultiCertConfigLoader::init_server_ssl_ctx(std::vector &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; } } @@ -1695,7 +1702,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(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(nbytes)); + } else { + size_t nwrite; + ret = SSL_write_early_data(ssl, buf, static_cast(nbytes), &nwrite); + if (ret == 1) { + ret = nwrite; + } + } + } else { + ret = SSL_write(ssl, buf, static_cast(nbytes)); + } +#else + ret = SSL_write(ssl, buf, static_cast(nbytes)); +#endif + if (ret > 0) { nwritten = ret; BIO *bio = SSL_get_wbio(ssl); @@ -1723,6 +1749,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(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(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(nbytes)); if (ret > 0) { nread = ret; @@ -1743,11 +1826,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(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 794b6acf41f..881f9bd4656 100644 --- a/mgmt/RecordsConfig.cc +++ b/mgmt/RecordsConfig.cc @@ -1149,7 +1149,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 73843e8c7a9..8042c3430c0 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 60a99940ffc..67defe37fc5 100644 --- a/proxy/ProxyTransaction.h +++ b/proxy/ProxyTransaction.h @@ -37,7 +37,7 @@ class ProxyTransaction : public VConnection /// 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 30fcf6c9ae5..ed52fccbab8 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 @@ class HTTPHdr : public MIMEHdr /// 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 @@ class HTTPHdr : public MIMEHdr 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); @@ -1199,6 +1205,26 @@ HTTPHdr::reason_set(const char *value, int length) http_hdr_reason_set(m_heap, m_http, value, length, true); } +/*------------------------------------------------------------------------- + -------------------------------------------------------------------------*/ + +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; +} + /*------------------------------------------------------------------------- -------------------------------------------------------------------------*/ diff --git a/proxy/hdrs/HdrToken.cc b/proxy/hdrs/HdrToken.cc index a1ddec4349c..fcfc5c656b6 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 71074012b72..75cab70bbcc 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 1752ad39626..a1e9dbc2c88 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 d5b2269ba20..38705dffecf 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(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 fd9afda0494..4fa61c64f9e 100644 --- a/proxy/http/Http1ClientSession.h +++ b/proxy/http/Http1ClientSession.h @@ -124,6 +124,8 @@ class Http1ClientSession : public ProxySession int released_transactions = 0; + int64_t read_from_early_data = 0; + public: // Link debug_link; LINK(Http1ClientSession, debug_link); diff --git a/proxy/http/HttpSM.cc b/proxy/http/HttpSM.cc index c71be4aafb8..a862d5a3f2a 100644 --- a/proxy/http/HttpSM.cc +++ b/proxy/http/HttpSM.cc @@ -343,10 +343,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 @@ -706,7 +708,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; @@ -797,6 +799,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 313c86f1bd9..b00d64d1d29 100644 --- a/proxy/http/HttpSM.h +++ b/proxy/http/HttpSM.h @@ -214,7 +214,7 @@ class HttpSM : public Continuation 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); @@ -634,6 +634,7 @@ class HttpSM : public Continuation 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 72c129218c7..8e792348c61 100644 --- a/proxy/http/HttpTransact.cc +++ b/proxy/http/HttpTransact.cc @@ -569,6 +569,16 @@ HttpTransact::Forbidden(State *s) TRANSACT_RETURN(SM_ACTION_SEND_ERROR_CACHE_NOOP, nullptr); } +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) { @@ -7595,6 +7605,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 68ec711e93e..cf57d7023e6 100644 --- a/proxy/http/HttpTransact.h +++ b/proxy/http/HttpTransact.h @@ -940,6 +940,7 @@ class HttpTransact 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 ed5069c3c47..96f442085a1 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(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 eee6a3b6a2f..c3a7209e077 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 @@ class Http2Frame } } + bool + is_from_early_data() const + { + return this->from_early_data; + } + // noncopyable Http2Frame(Http2Frame &) = delete; Http2Frame &operator=(const Http2Frame &) = delete; @@ -155,6 +164,7 @@ class Http2Frame Http2FrameHeader hdr; // frame header Ptr ioblock; // frame payload IOBufferReader *ioreader = nullptr; + bool from_early_data = false; }; class Http2ClientSession : public ProxySession @@ -264,6 +274,9 @@ class Http2ClientSession : public ProxySession 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 http2ClientSessionAllocator; diff --git a/proxy/http2/Http2ConnectionState.cc b/proxy/http2/Http2ConnectionState.cc index 7fb41ce6f64..fc683a4a112 100644 --- a/proxy/http2/Http2ConnectionState.cc +++ b/proxy/http2/Http2ConnectionState.cc @@ -358,7 +358,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); } @@ -914,7 +914,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 { @@ -1009,6 +1011,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 1c290eb6934..69e23506ee0 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 00000000000..047f0a308d6 --- /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 00000000000..e85fd178d66 --- /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 00000000000..6f535e8fc6b 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 00000000000..71c3350686a 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 00000000000..cdd633a7af2 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 00000000000..ecee5c75455 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 00000000000..7f8752760a9 --- /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 00000000000..37e63c23f6c --- /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 00000000000..05c71651b27 --- /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 00000000000..e4388461c3a --- /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-SHA256:DHE-RSA-AES128-SHA256:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-DSS-AES256-SHA:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA' +}) + +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', '')