diff --git a/.github/configs/os-check-linux.json b/.github/configs/os-check-linux.json index 95997da76c..6dc60c3cd2 100644 --- a/.github/configs/os-check-linux.json +++ b/.github/configs/os-check-linux.json @@ -16,6 +16,9 @@ "configure": ["--enable-all", "--enable-haproxy", "--enable-quic"]}, {"name": "all-asn-template-old-oid-sum", "minutes": 8.5, "configure": ["--enable-all", "--enable-asn=template", "CPPFLAGS=-DWOLFSSL_OLD_OID_SUM"]}, +{"name": "all-tls13-middlebox-compat", "minutes": 8.5, + "comment": "Only --enable-tls13-middlebox-compat and --enable-jni define WOLFSSL_TLS13_MIDDLEBOX_COMPAT, and configure.ac turns it back off when ECH is on, which --enable-all enables. So no other entry in this matrix compiles the client-side ChangeCipherSpec sends in wolfSSL_connect_TLSv13 or the tests/api/test_tls13.c middlebox tests guarded on that macro; without --disable-ech here they would all be skipped.", + "configure": ["--enable-all", "--disable-ech", "--enable-tls13-middlebox-compat"]}, {"name": "all-asn-original-old-oid-sum", "minutes": 8.4, "configure": ["--enable-all", "--enable-asn=original", "CPPFLAGS=-DWOLFSSL_OLD_OID_SUM"]}, {"name": "all-asn-original", "minutes": 8.3, diff --git a/CMakeLists.txt b/CMakeLists.txt index a8074b5b68..f72b161067 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -734,7 +734,7 @@ add_option("WOLFSSL_TLS13_SHA512" # TLS v1.3 middlebox compatibility (RFC 8446 Appendix D.4) add_option("WOLFSSL_TLS13_MIDDLEBOX_COMPAT" - "Enable TLS v1.3 middlebox compatibility mode (default: disabled)" + "Enable client-side TLS v1.3 middlebox compatibility: send a fake session id and a ChangeCipherSpec. Server-side is always on (default: disabled)" "no" "yes;no") if(WOLFSSL_TLS13_MIDDLEBOX_COMPAT) @@ -3160,7 +3160,7 @@ if(WOLFSSL_ECH) list(APPEND WOLFSSL_DEFINITIONS "-DHAVE_ECH") if(WOLFSSL_TLS13_MIDDLEBOX_COMPAT) message(WARNING - "ECH is incompatible with middlebox compatibility - disabling middlebox compatibility") + "ECH is incompatible with client-side middlebox compatibility - disabling middlebox compatibility") override_cache(WOLFSSL_TLS13_MIDDLEBOX_COMPAT "no") list(REMOVE_ITEM WOLFSSL_DEFINITIONS "-DWOLFSSL_TLS13_MIDDLEBOX_COMPAT") diff --git a/ChangeLog.md b/ChangeLog.md index f185964342..936fc7bad5 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -233,6 +233,19 @@ key of a reloaded key must keep the one exported at generation time, or load it into a separate key with `ImportPubRaw`. +* **Behavioral change (TLS 1.3 server ChangeCipherSpec)**: a TLS 1.3 server + now answers a ClientHello carrying a non-empty `legacy_session_id` with a + ChangeCipherSpec record, as RFC 8446 Appendix D.4 describes for middlebox + compatibility mode. It only did so before in builds defining + `WOLFSSL_TLS13_MIDDLEBOX_COMPAT`, which only `--enable-tls13-middlebox-compat` + and `--enable-jni` set and neither is on by default, so a default-built + server stayed silent and peers that expect the record - Erlang's `ssl` + among them - aborted the handshake with an unexpected_message alert. A + client sending an empty session id still sees none, and DTLS and QUIC are + unaffected. One visible difference: `wolfSSL_get_state()` no longer passes + through `WOLFSSL_SS_SERVER_CHANGECIPHERSPEC` or + `WOLFSSL_SS_CLIENT_CHANGECIPHERSPEC` on a TLS 1.3 connection. + ## New Features * Added `WC_ALGO_TYPE_KEYSTORE`, a crypto callback algorithm type for lifetime operations on keys held in a hardware key store, with the public API in `wolfssl/wolfcrypt/wc_keystore.h` behind `--enable-cryptocbutils=keystore`. Seven operations - plaintext and wrapped import/export, derive, delete and get-info - address keys by an opaque device-defined reference that wolfCrypt copies through and never interprets, the same way it treats a key object's `id[]` blob. This lets a device create, wrap, derive and destroy keys that never appear in memory, which `WOLF_CRYPTO_CB_SETKEY` and `WOLF_CRYPTO_CB_EXPORT_KEY` cannot express because both are bound to a wolfCrypt key object holding material for its own use. diff --git a/configure.ac b/configure.ac index c669e96a79..0028f10ba0 100644 --- a/configure.ac +++ b/configure.ac @@ -1429,7 +1429,8 @@ then if test "$KERNEL_MODE_DEFAULTS" != "yes" then - # Disable QUIC with JNI since incompatible with WOLFSSL_TLS13_MIDDLEBOX_COMPAT + # Disable QUIC with JNI since incompatible with client-side + # WOLFSSL_TLS13_MIDDLEBOX_COMPAT test "$enable_quic" = "" && test "$enable_cryptonly" != "yes" && test "$enable_jni" != "yes" && enable_quic=yes AM_CFLAGS="$AM_CFLAGS -DHAVE_CRL_IO -DHAVE_IO_TIMEOUT" fi @@ -1447,7 +1448,8 @@ then if test "$ENABLED_FIPS" = "no" then - # Disable ECH with JNI since incompatible with WOLFSSL_TLS13_MIDDLEBOX_COMPAT + # Disable ECH with JNI since incompatible with client-side + # WOLFSSL_TLS13_MIDDLEBOX_COMPAT test "$enable_ech" = "" && test "$enable_jni" != "yes" && enable_ech=yes test "$enable_scep" = "" && enable_scep=yes test "$enable_mcast" = "" && enable_mcast=yes @@ -2754,7 +2756,7 @@ AC_ARG_ENABLE([tls13-sha512], # TLS v1.3 middlebox compatibility (RFC 8446 Appendix D.4) AC_ARG_ENABLE([tls13-middlebox-compat], - [AS_HELP_STRING([--enable-tls13-middlebox-compat],[Enable TLS v1.3 middlebox compatibility mode (default: disabled)])], + [AS_HELP_STRING([--enable-tls13-middlebox-compat],[Enable client-side TLS v1.3 middlebox compatibility: send a fake session id and a ChangeCipherSpec. Server-side is always on (default: disabled)])], [ ENABLED_TLS13_MIDDLEBOX_COMPAT=$enableval ], [ ENABLED_TLS13_MIDDLEBOX_COMPAT=no ] ) @@ -2766,7 +2768,7 @@ then ENABLED_TLS13_MIDDLEBOX_COMPAT="no" elif test "x$ENABLED_ECH" = "xyes" then - AC_MSG_NOTICE([ECH is incompatible with middlebox compatibility - disabling middlebox compatibility]) + AC_MSG_NOTICE([ECH is incompatible with client-side middlebox compatibility - disabling middlebox compatibility]) ENABLED_TLS13_MIDDLEBOX_COMPAT="no" else AM_CFLAGS="$AM_CFLAGS -DWOLFSSL_TLS13_MIDDLEBOX_COMPAT" @@ -9785,7 +9787,7 @@ then AM_CFLAGS="$AM_CFLAGS -DWOLFSSL_ALWAYS_KEEP_SNI" if test "x$ENABLED_ECH" = "xyes" then - AC_MSG_NOTICE([ECH is incompatible with middlebox compatibility - not enabling middlebox compatibility for JNI]) + AC_MSG_NOTICE([ECH is incompatible with client-side middlebox compatibility - not enabling middlebox compatibility for JNI]) else AM_CFLAGS="$AM_CFLAGS -DWOLFSSL_TLS13_MIDDLEBOX_COMPAT" fi diff --git a/src/internal.c b/src/internal.c index cfe7d56517..1c68f82969 100644 --- a/src/internal.c +++ b/src/internal.c @@ -8493,9 +8493,8 @@ static void InitSSL_Tls13Options(WOLFSSL* ssl, WOLFSSL_CTX* ctx) ssl->numGroups = ctx->numGroups; } - #ifdef WOLFSSL_TLS13_MIDDLEBOX_COMPAT - ssl->options.tls13MiddleBoxCompat = 1; - #endif + /* Server clears this when the ClientHello legacy_session_id is empty. */ + ssl->options.tls13MiddleBoxCompat = 1; } #endif /* WOLFSSL_TLS13 */ @@ -26325,8 +26324,12 @@ int ProcessReplyEx(WOLFSSL* ssl, int allowSocketErr) return ret; } +/* The TLS 1.3 server accept path calls this for RFC 8446 Appendix D.4, so a + * server needs it whenever TLS 1.3 is built, not only with middlebox compat. + * A TLS 1.3 client only sends one in a middlebox compat build. */ #if !defined(WOLFSSL_NO_TLS12) || !defined(NO_OLD_TLS) || \ - (defined(WOLFSSL_TLS13) && defined(WOLFSSL_TLS13_MIDDLEBOX_COMPAT)) + (defined(WOLFSSL_TLS13) && (!defined(NO_WOLFSSL_SERVER) || \ + defined(WOLFSSL_TLS13_MIDDLEBOX_COMPAT))) int SendChangeCipher(WOLFSSL* ssl) { byte *output; @@ -26337,13 +26340,17 @@ int SendChangeCipher(WOLFSSL* ssl) #ifdef OPENSSL_EXTRA ssl->cbmode = WOLFSSL_CB_MODE_WRITE; if (ssl->options.side == WOLFSSL_SERVER_END){ - ssl->options.serverState = SERVER_CHANGECIPHERSPEC_COMPLETE; + /* A TLS 1.3 record here is a dummy and moves no state. */ + if (!IsAtLeastTLSv1_3(ssl->version)) + ssl->options.serverState = SERVER_CHANGECIPHERSPEC_COMPLETE; if (ssl->CBIS != NULL) ssl->CBIS(ssl, WOLFSSL_CB_ACCEPT_LOOP, WOLFSSL_SUCCESS); } else { - ssl->options.clientState = - CLIENT_CHANGECIPHERSPEC_COMPLETE; + /* As above; nothing on the TLS 1.3 connect path reads clientState. */ + if (!IsAtLeastTLSv1_3(ssl->version)) { + ssl->options.clientState = CLIENT_CHANGECIPHERSPEC_COMPLETE; + } if (ssl->CBIS != NULL) ssl->CBIS(ssl, WOLFSSL_CB_CONNECT_LOOP, WOLFSSL_SUCCESS); } @@ -26443,7 +26450,7 @@ int SendChangeCipher(WOLFSSL* ssl) return SendBuffered(ssl); } #endif /* !WOLFSSL_NO_TLS12 || !NO_OLD_TLS || - * (WOLFSSL_TLS13 && WOLFSSL_TLS13_MIDDLEBOX_COMPAT) */ + * (WOLFSSL_TLS13 && (!NO_WOLFSSL_SERVER || WOLFSSL_TLS13_MIDDLEBOX_COMPAT)) */ #if !defined(NO_OLD_TLS) && !defined(WOLFSSL_AEAD_ONLY) diff --git a/src/ssl.c b/src/ssl.c index 25d15c05c7..f937f482e4 100644 --- a/src/ssl.c +++ b/src/ssl.c @@ -5701,6 +5701,10 @@ size_t wolfSSL_get_client_random(const WOLFSSL* ssl, unsigned char* out, ssl->options.hrrSentCookie = 0; #endif ssl->options.hrrSentKeyShare = 0; + ssl->options.sentChangeCipher = 0; + /* Matches InitSSL_Tls13Options(); the server clears it again when the + * next ClientHello carries an empty session id. */ + ssl->options.tls13MiddleBoxCompat = 1; #endif #ifdef WOLFSSL_DTLS ssl->options.dtlsStateful = 0; diff --git a/src/tls13.c b/src/tls13.c index 74c7691c6c..7a188c3d3b 100644 --- a/src/tls13.c +++ b/src/tls13.c @@ -33,8 +33,13 @@ * WOLFSSL_DTLS_CH_FRAG: Enable DTLS 1.3 ClientHello frag default: off * * Handshake: - * WOLFSSL_TLS13_MIDDLEBOX_COMPAT: Enable middlebox compatibility default: on - * Sends ChangeCipherSpec and includes session id + * WOLFSSL_TLS13_MIDDLEBOX_COMPAT: Client-side middlebox compatibility + * default: off + * Makes the client send a fake session id and its + * own ChangeCipherSpec. The server always answers + * a non-empty client session id with a + * ChangeCipherSpec, as RFC 8446 Appendix D.4 + * requires, whether or not this is defined. * WOLFSSL_SEND_HRR_COOKIE: Send cookie in HelloRetryRequest default: off * for stateless ClientHello tracking. A client * always echoes back a cookie it is sent. @@ -8071,12 +8076,17 @@ int DoTls13ClientHello(WOLFSSL* ssl, const byte* input, word32* inOutIdx, } args->idx += sessIdSz; -#ifdef WOLFSSL_TLS13_MIDDLEBOX_COMPAT /* RFC 8446 Appendix D.4: server MUST only send CCS if the client's - * ClientHello contains a non-empty legacy_session_id. */ + * ClientHello contains a non-empty legacy_session_id. An ECH inner hello + * is rebuilt with the outer session id, so it decides either way. */ if (sessIdSz == 0) { ssl->options.tls13MiddleBoxCompat = 0; } +#ifdef WOLFSSL_QUIC + /* RFC 9001 Section 8.4: QUIC has no compatibility mode to be had. */ + if (WOLFSSL_IS_QUIC(ssl)) { + ssl->options.tls13MiddleBoxCompat = 0; + } #endif #ifdef WOLFSSL_DTLS13 @@ -16409,13 +16419,20 @@ int wolfSSL_connect_TLSv13(WOLFSSL* ssl) if (ssl->earlyData != no_early_data && ssl->options.handShakeState != CLIENT_HELLO_COMPLETE) { #if defined(WOLFSSL_TLS13_MIDDLEBOX_COMPAT) - if (!ssl->options.dtls && - ssl->options.tls13MiddleBoxCompat) { - if ((ssl->error = SendChangeCipher(ssl)) != 0) { + if (!ssl->options.dtls && !ssl->options.sentChangeCipher + && ssl->options.tls13MiddleBoxCompat) { + ssl->error = SendChangeCipher(ssl); + /* A short send leaves the record queued in the output + * buffer, so a resumed connect must not build a second + * one. Same on every other site. */ + if (ssl->error == 0 || + ssl->error == WC_NO_ERR_TRACE(WANT_WRITE)) { + ssl->options.sentChangeCipher = 1; + } + if (ssl->error != 0) { WOLFSSL_ERROR(ssl->error); return WOLFSSL_FATAL_ERROR; } - ssl->options.sentChangeCipher = 1; } #endif ssl->options.handShakeState = CLIENT_HELLO_COMPLETE; @@ -16461,11 +16478,19 @@ int wolfSSL_connect_TLSv13(WOLFSSL* ssl) #if defined(WOLFSSL_TLS13_MIDDLEBOX_COMPAT) if (!ssl->options.dtls && !ssl->options.sentChangeCipher && ssl->options.tls13MiddleBoxCompat) { - if ((ssl->error = SendChangeCipher(ssl)) != 0) { + ssl->error = SendChangeCipher(ssl); + if (ssl->error == 0 || + ssl->error == WC_NO_ERR_TRACE(WANT_WRITE)) { + ssl->options.sentChangeCipher = 1; + } + if (ssl->error != 0) { + /* The second ClientHello still has to follow, so hold + * the state machine on this case while the record + * drains. */ + ssl->options.buildingMsg = 1; WOLFSSL_ERROR(ssl->error); return WOLFSSL_FATAL_ERROR; } - ssl->options.sentChangeCipher = 1; } #endif /* Try again with different security parameters. */ @@ -16542,11 +16567,15 @@ int wolfSSL_connect_TLSv13(WOLFSSL* ssl) #if defined(WOLFSSL_TLS13_MIDDLEBOX_COMPAT) if (!ssl->options.sentChangeCipher && !ssl->options.dtls && ssl->options.tls13MiddleBoxCompat) { - if ((ssl->error = SendChangeCipher(ssl)) != 0) { + ssl->error = SendChangeCipher(ssl); + if (ssl->error == 0 || + ssl->error == WC_NO_ERR_TRACE(WANT_WRITE)) { + ssl->options.sentChangeCipher = 1; + } + if (ssl->error != 0) { WOLFSSL_ERROR(ssl->error); return WOLFSSL_FATAL_ERROR; } - ssl->options.sentChangeCipher = 1; } #endif @@ -17551,6 +17580,39 @@ const char* wolfSSL_get_cipher_name_by_hash(WOLFSSL* ssl, const char* hash) #ifndef NO_WOLFSSL_SERVER +/* Send the RFC 8446 Appendix D.4 ChangeCipherSpec a server owes a client that + * offered a non-empty legacy_session_id. + * + * ssl The SSL/TLS object. + * flush Force the record out on its own. A HelloRetryRequest needs this + * because SendTls13ServerHello has already sent it, unlike a + * ServerHello, which waits for the rest of its flight. + * returns 0 on success. + */ +static int SendTls13ServerChangeCipher(WOLFSSL* ssl, int flush) +{ + int ret; + + /* DoTls13ClientHello clears tls13MiddleBoxCompat for a QUIC peer, so the + * check here is a local backstop, 0 when QUIC is not built. */ + if (ssl->options.dtls || WOLFSSL_IS_QUIC(ssl) + || !ssl->options.tls13MiddleBoxCompat + || ssl->options.sentChangeCipher) { + return 0; + } + + ret = SendChangeCipher(ssl); + /* A short send leaves the record queued in the output buffer. Mark it sent + * anyway, or the resumed accept, which comes back in at the ServerHello + * case, puts a second record on the wire. */ + if (ret == 0 || ret == WC_NO_ERR_TRACE(WANT_WRITE)) + ssl->options.sentChangeCipher = 1; + if (ret == 0 && flush && ssl->options.groupMessages) + ret = SendBuffered(ssl); + + return ret; +} + /* The server accepting a connection from a client. * The protocol version is expecting to be TLS v1.3. * If the client downgrades, and older versions of the protocol are compiled @@ -17792,18 +17854,14 @@ int wolfSSL_accept_TLSv13(WOLFSSL* ssl) FALL_THROUGH; case TLS13_ACCEPT_HELLO_RETRY_REQUEST_DONE : - #ifdef WOLFSSL_TLS13_MIDDLEBOX_COMPAT - if (!ssl->options.dtls && ssl->options.tls13MiddleBoxCompat - && ssl->options.serverState == + if (ssl->options.serverState == SERVER_HELLO_RETRY_REQUEST_COMPLETE) { - if ((ssl->error = SendChangeCipher(ssl)) != 0) { + ssl->error = SendTls13ServerChangeCipher(ssl, 1); + if (ssl->error != 0) { WOLFSSL_ERROR(ssl->error); return WOLFSSL_FATAL_ERROR; } - ssl->options.sentChangeCipher = 1; - ssl->options.serverState = SERVER_HELLO_RETRY_REQUEST_COMPLETE; } - #endif ssl->options.acceptState = TLS13_ACCEPT_FIRST_REPLY_DONE; WOLFSSL_MSG("accept state ACCEPT_FIRST_REPLY_DONE"); FALL_THROUGH; @@ -17851,16 +17909,11 @@ int wolfSSL_accept_TLSv13(WOLFSSL* ssl) FALL_THROUGH; case TLS13_SERVER_HELLO_SENT : - #if defined(WOLFSSL_TLS13_MIDDLEBOX_COMPAT) - if (!ssl->options.dtls && ssl->options.tls13MiddleBoxCompat - && !ssl->options.sentChangeCipher && !ssl->options.dtls) { - if ((ssl->error = SendChangeCipher(ssl)) != 0) { - WOLFSSL_ERROR(ssl->error); - return WOLFSSL_FATAL_ERROR; - } - ssl->options.sentChangeCipher = 1; + ssl->error = SendTls13ServerChangeCipher(ssl, 0); + if (ssl->error != 0) { + WOLFSSL_ERROR(ssl->error); + return WOLFSSL_FATAL_ERROR; } - #endif ssl->options.acceptState = TLS13_ACCEPT_THIRD_REPLY_DONE; WOLFSSL_MSG("accept state ACCEPT_THIRD_REPLY_DONE"); diff --git a/tests/api/test_tls13.c b/tests/api/test_tls13.c index d3376c056b..0b4eb9abba 100644 --- a/tests/api/test_tls13.c +++ b/tests/api/test_tls13.c @@ -7114,20 +7114,41 @@ static int Tls13PTARecv(WOLFSSL* ssl, char* buf, int sz, void* ctx) } #endif +#if defined(WOLFSSL_TLS13) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) +/* How many ChangeCipherSpec records does this buffer of TLS records carry? + * A count rather than a yes or no, so that a second record going out, which + * is what marking the record sent before the send guards against, does not + * read the same as the one record that is owed. */ +static int test_tls13_count_ccs(const byte* buff, int len) +{ + int i = 0; + int count = 0; + + while (i + RECORD_HEADER_SZ <= len) { + if (buff[i] == change_cipher_spec) + count++; + i += RECORD_HEADER_SZ + ((buff[i + 3] << 8) | buff[i + 4]); + } + + return count; +} +#endif + /* Test that when a TLS 1.3 client sends a ClientHello with an empty * legacy_session_id (indicating no middlebox compatibility), the server * should NOT send a ChangeCipherSpec message. Per RFC 8446 Appendix D.4, * the server only sends CCS if the client's ClientHello contains a * non-empty session_id. * - * This test reproduces the bug reported in GitHub issue #9156 where - * wolfSSL server always sends CCS when compiled with - * WOLFSSL_TLS13_MIDDLEBOX_COMPAT, regardless of the client's session_id. + * This test reproduces the bug reported in GitHub issue #9156, where the + * server sent a ChangeCipherSpec whatever the client's session_id was. */ int test_tls13_middlebox_compat_empty_session_id(void) { EXPECT_DECLS; -#if defined(WOLFSSL_TLS13) && defined(WOLFSSL_TLS13_MIDDLEBOX_COMPAT) && \ +#if defined(WOLFSSL_TLS13) && \ defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) WOLFSSL_CTX *ctx_c = NULL; @@ -7135,16 +7156,15 @@ int test_tls13_middlebox_compat_empty_session_id(void) WOLFSSL *ssl_c = NULL; WOLFSSL *ssl_s = NULL; struct test_memio_ctx test_ctx; - int i; - int found_ccs = 0; XMEMSET(&test_ctx, 0, sizeof(test_ctx)); ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), 0); - /* Disable middlebox compatibility on the client so it sends an empty - * legacy_session_id in ClientHello. The server should respect this and - * NOT send a ChangeCipherSpec. */ + /* Without WOLFSSL_TLS13_MIDDLEBOX_COMPAT the client already sends an + * empty legacy_session_id; clearing the flag covers the build where it + * would otherwise send a fake one. Either way the server must respect + * the empty id and NOT send a ChangeCipherSpec. */ if (EXPECT_SUCCESS()) { ssl_c->options.tls13MiddleBoxCompat = 0; } @@ -7161,30 +7181,10 @@ int test_tls13_middlebox_compat_empty_session_id(void) ExpectIntEQ(wolfSSL_get_error(ssl_s, WC_NO_ERR_TRACE(WOLFSSL_FATAL_ERROR)), WOLFSSL_ERROR_WANT_READ); - /* Now examine the server's output (stored in c_buff, since the server - * writes to the client's read buffer). Scan through TLS records looking - * for a ChangeCipherSpec record (content type 0x14 = 20). */ - if (EXPECT_SUCCESS()) { - i = 0; - while (i + 5 <= test_ctx.c_len) { - byte content_type = test_ctx.c_buff[i]; - int record_len = (test_ctx.c_buff[i + 3] << 8) | - test_ctx.c_buff[i + 4]; - - if (content_type == 20) { /* change_cipher_spec */ - found_ccs = 1; - break; - } - - /* Move to next TLS record: 5 byte header + payload */ - i += 5 + record_len; - } - } - - /* The server should NOT have sent CCS since the client's ClientHello - * had an empty legacy_session_id. If found_ccs is 1, this demonstrates + /* The server writes into c_buff. It must NOT have sent a CCS, since the + * client's ClientHello had an empty legacy_session_id. Sending one is * the bug from issue #9156. */ - ExpectIntEQ(found_ccs, 0); + ExpectIntEQ(test_tls13_count_ccs(test_ctx.c_buff, test_ctx.c_len), 0); wolfSSL_free(ssl_c); wolfSSL_free(ssl_s); @@ -7237,6 +7237,557 @@ int test_tls13_middlebox_compat_session_id(void) return EXPECT_RESULT(); } +int test_tls13_middlebox_compat_server_ccs(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_TLS13) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) + WOLFSSL_CTX *ctx_c = NULL; + WOLFSSL_CTX *ctx_s = NULL; + WOLFSSL *ssl_c = NULL; + WOLFSSL *ssl_s = NULL; + struct test_memio_ctx test_ctx; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), 0); + + /* Give the client a non-empty legacy_session_id. GetTls13SessionId puts + * it on the wire whatever the build options are, which is what drives + * the server behaviour RFC 8446 Appendix D.4 requires. */ + if (EXPECT_SUCCESS()) { + XMEMSET(ssl_c->session->sessionID, 0xA5, ID_LEN); + ssl_c->session->sessionIDSz = ID_LEN; + } + + /* Client sends ClientHello with a non-empty session id. */ + ExpectIntNE(wolfSSL_connect(ssl_c), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_get_error(ssl_c, + WC_NO_ERR_TRACE(WOLFSSL_FATAL_ERROR)), WOLFSSL_ERROR_WANT_READ); + + /* Server processes it and writes its first flight. */ + ExpectIntNE(wolfSSL_accept(ssl_s), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_get_error(ssl_s, + WC_NO_ERR_TRACE(WOLFSSL_FATAL_ERROR)), WOLFSSL_ERROR_WANT_READ); + + /* The client offered a non-empty session id, so the server owes it a + * ChangeCipherSpec. Peers such as Erlang's ssl abort the handshake with + * an unexpected_message alert when it is missing. */ + ExpectIntEQ(test_tls13_count_ccs(test_ctx.c_buff, test_ctx.c_len), 1); + + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} + +/* wolfSSL_clear() must not leave the previous handshake's ChangeCipherSpec + * state behind, or a reused server object stops answering a non-empty + * legacy_session_id after the first handshake. */ +int test_tls13_middlebox_compat_server_reuse(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_TLS13) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) + WOLFSSL_CTX *ctx_c = NULL; + WOLFSSL_CTX *ctx_s = NULL; + WOLFSSL *ssl_c = NULL; + WOLFSSL *ssl_c2 = NULL; + WOLFSSL *ssl_s = NULL; + struct test_memio_ctx test_ctx; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), 0); + + if (EXPECT_SUCCESS()) { + XMEMSET(ssl_c->session->sessionID, 0xA5, ID_LEN); + ssl_c->session->sessionIDSz = ID_LEN; + } + + ExpectIntNE(wolfSSL_connect(ssl_c), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_get_error(ssl_c, + WC_NO_ERR_TRACE(WOLFSSL_FATAL_ERROR)), WOLFSSL_ERROR_WANT_READ); + ExpectIntNE(wolfSSL_accept(ssl_s), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_get_error(ssl_s, + WC_NO_ERR_TRACE(WOLFSSL_FATAL_ERROR)), WOLFSSL_ERROR_WANT_READ); + ExpectIntEQ(test_tls13_count_ccs(test_ctx.c_buff, test_ctx.c_len), 1); + + /* Reuse the server object for a second handshake with a fresh client. */ + ExpectIntEQ(wolfSSL_clear(ssl_s), WOLFSSL_SUCCESS); + test_memio_clear_buffer(&test_ctx, 0); + test_memio_clear_buffer(&test_ctx, 1); + + ExpectNotNull(ssl_c2 = wolfSSL_new(ctx_c)); + if (EXPECT_SUCCESS()) { + wolfSSL_SetIOWriteCtx(ssl_c2, &test_ctx); + wolfSSL_SetIOReadCtx(ssl_c2, &test_ctx); + XMEMSET(ssl_c2->session->sessionID, 0x5A, ID_LEN); + ssl_c2->session->sessionIDSz = ID_LEN; + } + + ExpectIntNE(wolfSSL_connect(ssl_c2), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_get_error(ssl_c2, + WC_NO_ERR_TRACE(WOLFSSL_FATAL_ERROR)), WOLFSSL_ERROR_WANT_READ); + ExpectIntNE(wolfSSL_accept(ssl_s), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_get_error(ssl_s, + WC_NO_ERR_TRACE(WOLFSSL_FATAL_ERROR)), WOLFSSL_ERROR_WANT_READ); + + /* The second peer also offered a session id, so it is owed a CCS too. */ + ExpectIntEQ(test_tls13_count_ccs(test_ctx.c_buff, test_ctx.c_len), 1); + + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_c2); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} + +#if defined(WOLFSSL_TLS13) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) +/* One HelloRetryRequest handshake, with the server's messages grouped or + * not. Its own verdict, so a failure in one setting still leaves the other + * exercised. */ +static int test_tls13_hrr_ccs_grouped(int grouped) +{ + EXPECT_DECLS; + WOLFSSL_CTX *ctx_c = NULL; + WOLFSSL_CTX *ctx_s = NULL; + WOLFSSL *ssl_c = NULL; + WOLFSSL *ssl_s = NULL; + struct test_memio_ctx test_ctx; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), 0); + + if (EXPECT_SUCCESS()) { + XMEMSET(ssl_c->session->sessionID, 0xA5, ID_LEN); + ssl_c->session->sessionIDSz = ID_LEN; + ssl_s->options.groupMessages = (byte)grouped; + } + + /* No key share entries, so the server has to ask for one. */ + ExpectIntEQ(wolfSSL_NoKeyShares(ssl_c), WOLFSSL_SUCCESS); + + ExpectIntNE(wolfSSL_connect(ssl_c), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_get_error(ssl_c, + WC_NO_ERR_TRACE(WOLFSSL_FATAL_ERROR)), WOLFSSL_ERROR_WANT_READ); + + ExpectIntNE(wolfSSL_accept(ssl_s), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_get_error(ssl_s, + WC_NO_ERR_TRACE(WOLFSSL_FATAL_ERROR)), WOLFSSL_ERROR_WANT_READ); + + /* The HelloRetryRequest flight carries exactly one, however the + * server groups its messages. */ + ExpectIntEQ(ssl_s->options.serverState, + SERVER_HELLO_RETRY_REQUEST_COMPLETE); + ExpectIntEQ(test_tls13_count_ccs(test_ctx.c_buff, test_ctx.c_len), 1); + + /* Step to the second ClientHello and the flight answering it. The + * client drains c_buff as it reads, so what is left is that flight + * alone: the server owes no second record and must not send one. */ + ExpectIntNE(wolfSSL_connect(ssl_c), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_get_error(ssl_c, + WC_NO_ERR_TRACE(WOLFSSL_FATAL_ERROR)), WOLFSSL_ERROR_WANT_READ); + ExpectIntNE(wolfSSL_accept(ssl_s), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_get_error(ssl_s, + WC_NO_ERR_TRACE(WOLFSSL_FATAL_ERROR)), WOLFSSL_ERROR_WANT_READ); + ExpectIntEQ(test_tls13_count_ccs(test_ctx.c_buff, test_ctx.c_len), 0); + + /* And the handshake the retry started still completes. */ + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); + return EXPECT_RESULT(); +} +#endif + +/* A HelloRetryRequest is flushed as soon as it is built, so the + * ChangeCipherSpec that follows it has to be flushed too. With grouped + * messages, which every OPENSSL_COMPATIBLE_DEFAULTS build turns on, it would + * otherwise sit in the output buffer and reach the peer a flight late. */ +int test_tls13_middlebox_compat_hrr_ccs(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_TLS13) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) + int ungrouped_res; + int grouped_res; + + /* Both run whatever the other does; both owe the same record. */ + ungrouped_res = test_tls13_hrr_ccs_grouped(0); + grouped_res = test_tls13_hrr_ccs_grouped(1); + ExpectIntEQ(ungrouped_res, TEST_SUCCESS); + ExpectIntEQ(grouped_res, TEST_SUCCESS); +#endif + return EXPECT_RESULT(); +} + +#if defined(WOLFSSL_TLS13) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) +/* Refuses one ChangeCipherSpec write, then counts the ones that land. */ +struct test_tls13_ccs_ctx { + struct test_memio_ctx* memio; + int refuse; + int sent; +}; + +/* Refuse the write carrying the ChangeCipherSpec, once, and let every other + * write through. Refusing by record type rather than by write count keeps + * the test tied to the record it is about. */ +static int test_tls13_ccs_write_cb(WOLFSSL* ssl, char* data, int sz, void* ctx) +{ + struct test_tls13_ccs_ctx* cctx = (struct test_tls13_ccs_ctx*)ctx; + int ret; + + if (sz > 0 && (byte)data[0] == change_cipher_spec && cctx->refuse) { + cctx->refuse = 0; + return WOLFSSL_CBIO_ERR_WANT_WRITE; + } + + ret = test_memio_write_cb(ssl, data, sz, cctx->memio); + if (ret > 0 && sz > 0 && (byte)data[0] == change_cipher_spec) + cctx->sent++; + + return ret; +} +#endif + +/* A server send that stops short must not put a second ChangeCipherSpec on + * the wire when it is resumed. Drive that path with a transport that refuses + * that one write. The HelloRetryRequest flavour below is the one that pins + * where sentChangeCipher is set; this one covers the straight-through + * flight, which the accept-state prologue recovers on its own. */ +int test_tls13_middlebox_compat_server_ccs_retry(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_TLS13) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) + WOLFSSL_CTX *ctx_c = NULL; + WOLFSSL_CTX *ctx_s = NULL; + WOLFSSL *ssl_c = NULL; + WOLFSSL *ssl_s = NULL; + struct test_memio_ctx test_ctx; + struct test_tls13_ccs_ctx ccs_ctx; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + XMEMSET(&ccs_ctx, 0, sizeof(ccs_ctx)); + ccs_ctx.memio = &test_ctx; + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), 0); + + if (EXPECT_SUCCESS()) { + XMEMSET(ssl_c->session->sessionID, 0xA5, ID_LEN); + ssl_c->session->sessionIDSz = ID_LEN; + /* Ungrouped, so the record is flushed on its own and the refusal + * below lands on it rather than on a flight carrying it along, and + * without the automatic retry an OPENSSL_COMPATIBLE_DEFAULTS build + * turns on, which would resume inside the send instead of through + * the accept state machine this test is about. */ + ssl_s->options.groupMessages = 0; +#if defined(OPENSSL_EXTRA) || defined(HAVE_WEBSERVER) || \ + defined(HAVE_MEMCACHED) + /* Only a build with the mode API can have turned auto retry on, and + * an OPENSSL_COMPATIBLE_DEFAULTS one has. Resuming the send has to + * come back through the accept state machine, not inside it. */ + wolfSSL_CTX_clear_mode(ctx_s, WOLFSSL_MODE_AUTO_RETRY); +#endif + wolfSSL_SSLSetIOSend(ssl_s, test_tls13_ccs_write_cb); + wolfSSL_SetIOWriteCtx(ssl_s, &ccs_ctx); + ccs_ctx.refuse = 1; + } + + ExpectIntNE(wolfSSL_connect(ssl_c), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_get_error(ssl_c, + WC_NO_ERR_TRACE(WOLFSSL_FATAL_ERROR)), WOLFSSL_ERROR_WANT_READ); + + /* The ChangeCipherSpec write is refused, so the flight stops on it. */ + ExpectIntNE(wolfSSL_accept(ssl_s), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_get_error(ssl_s, + WC_NO_ERR_TRACE(WOLFSSL_FATAL_ERROR)), WOLFSSL_ERROR_WANT_WRITE); + ExpectIntEQ(ccs_ctx.refuse, 0); + + /* Resume the same handshake, now that the write goes through. */ + ExpectIntNE(wolfSSL_accept(ssl_s), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_get_error(ssl_s, + WC_NO_ERR_TRACE(WOLFSSL_FATAL_ERROR)), WOLFSSL_ERROR_WANT_READ); + + /* One record, not two: the resumed accept must not repeat the send. */ + ExpectIntEQ(test_tls13_count_ccs(test_ctx.c_buff, test_ctx.c_len), 1); + + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} + +/* The HelloRetryRequest path marks the record sent before sending it, + * because a resumed accept falls through into TLS13_SERVER_HELLO_SENT and + * only that flag stops it sending a second one. Refuse the write, resume, + * and check the flight answering the second ClientHello stays empty. */ +int test_tls13_middlebox_compat_hrr_ccs_retry(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_TLS13) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) + WOLFSSL_CTX *ctx_c = NULL; + WOLFSSL_CTX *ctx_s = NULL; + WOLFSSL *ssl_c = NULL; + WOLFSSL *ssl_s = NULL; + struct test_memio_ctx test_ctx; + struct test_tls13_ccs_ctx ccs_ctx; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + XMEMSET(&ccs_ctx, 0, sizeof(ccs_ctx)); + ccs_ctx.memio = &test_ctx; + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), 0); + + if (EXPECT_SUCCESS()) { + XMEMSET(ssl_c->session->sessionID, 0xA5, ID_LEN); + ssl_c->session->sessionIDSz = ID_LEN; + ssl_s->options.groupMessages = 0; +#if defined(OPENSSL_EXTRA) || defined(HAVE_WEBSERVER) || \ + defined(HAVE_MEMCACHED) + /* Only a build with the mode API can have turned auto retry on, and + * an OPENSSL_COMPATIBLE_DEFAULTS one has. Resuming the send has to + * come back through the accept state machine, not inside it. */ + wolfSSL_CTX_clear_mode(ctx_s, WOLFSSL_MODE_AUTO_RETRY); +#endif + wolfSSL_SSLSetIOSend(ssl_s, test_tls13_ccs_write_cb); + wolfSSL_SetIOWriteCtx(ssl_s, &ccs_ctx); + ccs_ctx.refuse = 1; + } + + /* No key share entries, so the server has to ask for one. */ + ExpectIntEQ(wolfSSL_NoKeyShares(ssl_c), WOLFSSL_SUCCESS); + + ExpectIntNE(wolfSSL_connect(ssl_c), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_get_error(ssl_c, + WC_NO_ERR_TRACE(WOLFSSL_FATAL_ERROR)), WOLFSSL_ERROR_WANT_READ); + + /* The record behind the HelloRetryRequest is refused. */ + ExpectIntNE(wolfSSL_accept(ssl_s), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_get_error(ssl_s, + WC_NO_ERR_TRACE(WOLFSSL_FATAL_ERROR)), WOLFSSL_ERROR_WANT_WRITE); + ExpectIntEQ(ccs_ctx.refuse, 0); + + /* Resume: the retry owes exactly the one record it stopped on. */ + ExpectIntNE(wolfSSL_accept(ssl_s), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_get_error(ssl_s, + WC_NO_ERR_TRACE(WOLFSSL_FATAL_ERROR)), WOLFSSL_ERROR_WANT_READ); + ExpectIntEQ(test_tls13_count_ccs(test_ctx.c_buff, test_ctx.c_len), 1); + + /* The flight answering the second ClientHello owes none. A resumed + * accept falls into TLS13_SERVER_HELLO_SENT, so this is what catches + * the record being marked sent after the send rather than before. */ + ExpectIntNE(wolfSSL_connect(ssl_c), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_get_error(ssl_c, + WC_NO_ERR_TRACE(WOLFSSL_FATAL_ERROR)), WOLFSSL_ERROR_WANT_READ); + ExpectIntNE(wolfSSL_accept(ssl_s), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_get_error(ssl_s, + WC_NO_ERR_TRACE(WOLFSSL_FATAL_ERROR)), WOLFSSL_ERROR_WANT_READ); + ExpectIntEQ(test_tls13_count_ccs(test_ctx.c_buff, test_ctx.c_len), 0); + + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} + +#if defined(WOLFSSL_TLS13) && defined(WOLFSSL_TLS13_MIDDLEBOX_COMPAT) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) +/* One client handshake with its ChangeCipherSpec write refused once, either + * straight through or after a HelloRetryRequest. */ +static int test_tls13_client_ccs_retry_once(int noKeyShares) +{ + EXPECT_DECLS; + WOLFSSL_CTX *ctx_c = NULL; + WOLFSSL_CTX *ctx_s = NULL; + WOLFSSL *ssl_c = NULL; + WOLFSSL *ssl_s = NULL; + struct test_memio_ctx test_ctx; + struct test_tls13_ccs_ctx ccs_ctx; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + XMEMSET(&ccs_ctx, 0, sizeof(ccs_ctx)); + ccs_ctx.memio = &test_ctx; + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), 0); + + if (EXPECT_SUCCESS()) { + wolfSSL_SSLSetIOSend(ssl_c, test_tls13_ccs_write_cb); + wolfSSL_SetIOWriteCtx(ssl_c, &ccs_ctx); + /* Refuse the client's first ChangeCipherSpec write, wherever in the + * flight it falls. */ + ccs_ctx.refuse = 1; + } + if (noKeyShares) { + ExpectIntEQ(wolfSSL_NoKeyShares(ssl_c), WOLFSSL_SUCCESS); + } + + /* A refused record answering a HelloRetryRequest must not advance + * connectState past the second ClientHello. */ + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + ExpectIntEQ(ccs_ctx.sent, 1); + + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); + return EXPECT_RESULT(); +} +#endif + +/* The client marks its own middlebox ChangeCipherSpec sent before sending it, + * for the same reason the server does. Both flights get their own verdict. */ +int test_tls13_middlebox_compat_client_ccs_retry(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_TLS13) && defined(WOLFSSL_TLS13_MIDDLEBOX_COMPAT) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) + int hrr_res; + int direct_res; + + hrr_res = test_tls13_client_ccs_retry_once(1); + direct_res = test_tls13_client_ccs_retry_once(0); + ExpectIntEQ(hrr_res, TEST_SUCCESS); + ExpectIntEQ(direct_res, TEST_SUCCESS); +#endif + return EXPECT_RESULT(); +} + +/* The third client send site is the early data one, which puts the + * ChangeCipherSpec out before the 0-RTT records rather than as part of a + * handshake flight. Refuse that one write and resume: the record is owed + * once, and a resumed wolfSSL_write_early_data() must not repeat it. */ +int test_tls13_middlebox_compat_client_ccs_retry_early_data(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_TLS13) && defined(WOLFSSL_TLS13_MIDDLEBOX_COMPAT) && \ + defined(WOLFSSL_EARLY_DATA) && defined(HAVE_SESSION_TICKET) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) + WOLFSSL_CTX *ctx_c = NULL; + WOLFSSL_CTX *ctx_s = NULL; + WOLFSSL *ssl_c = NULL; + WOLFSSL *ssl_s = NULL; + WOLFSSL_SESSION *sess = NULL; + struct test_memio_ctx test_ctx; + struct test_tls13_ccs_ctx ccs_ctx; + const char earlyMsg[] = "This is early data"; + char buf[64]; + int written = 0; + int earlyRead = 0; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + XMEMSET(&ccs_ctx, 0, sizeof(ccs_ctx)); + ccs_ctx.memio = &test_ctx; + + /* A full handshake first, only to get a ticket to resume with; early + * data has nowhere else to come from. */ + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), 0); + + /* Opt the server into 0-RTT (off by default per RFC 8446 E.5). */ + ExpectIntGE(wolfSSL_CTX_set_max_early_data(ctx_s, MAX_EARLY_DATA_SZ), 0); + ExpectIntGE(wolfSSL_set_max_early_data(ssl_s, MAX_EARLY_DATA_SZ), 0); + + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + /* Read so that the NewSessionTicket is taken in. */ + ExpectIntEQ(wolfSSL_read(ssl_c, buf, sizeof(buf)), -1); + ExpectIntEQ(wolfSSL_get_error(ssl_c, -1), WOLFSSL_ERROR_WANT_READ); + ExpectNotNull(sess = wolfSSL_get1_session(ssl_c)); + + wolfSSL_free(ssl_c); + ssl_c = NULL; + wolfSSL_free(ssl_s); + ssl_s = NULL; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), 0); + ExpectIntGE(wolfSSL_set_max_early_data(ssl_s, MAX_EARLY_DATA_SZ), 0); + ExpectIntEQ(wolfSSL_set_session(ssl_c, sess), WOLFSSL_SUCCESS); + + if (EXPECT_SUCCESS()) { + /* Ungrouped, so the record is flushed on its own and the refusal + * below lands on it rather than on a flight carrying it along. */ + ssl_c->options.groupMessages = 0; +#if defined(OPENSSL_EXTRA) || defined(HAVE_WEBSERVER) || \ + defined(HAVE_MEMCACHED) + /* Only a build with the mode API can have turned auto retry on, and + * an OPENSSL_COMPATIBLE_DEFAULTS one has. Resuming the send has to + * come back through the connect state machine, not inside it. */ + wolfSSL_CTX_clear_mode(ctx_c, WOLFSSL_MODE_AUTO_RETRY); +#endif + wolfSSL_SSLSetIOSend(ssl_c, test_tls13_ccs_write_cb); + wolfSSL_SetIOWriteCtx(ssl_c, &ccs_ctx); + ccs_ctx.refuse = 1; + } + + /* The ChangeCipherSpec ahead of the early data is refused, so the + * client stops on it with nothing written. */ + ExpectIntEQ(wolfSSL_write_early_data(ssl_c, earlyMsg, (int)sizeof(earlyMsg), + &written), WC_NO_ERR_TRACE(WOLFSSL_FATAL_ERROR)); + ExpectIntEQ(wolfSSL_get_error(ssl_c, + WC_NO_ERR_TRACE(WOLFSSL_FATAL_ERROR)), WOLFSSL_ERROR_WANT_WRITE); + ExpectIntEQ(ccs_ctx.refuse, 0); + ExpectIntEQ(written, 0); + + /* Resume: the record it stopped on goes out, and only that one. */ + ExpectIntEQ(wolfSSL_write_early_data(ssl_c, earlyMsg, (int)sizeof(earlyMsg), + &written), (int)sizeof(earlyMsg)); + ExpectIntEQ(written, (int)sizeof(earlyMsg)); + ExpectIntEQ(ccs_ctx.sent, 1); + + /* The 0-RTT data the retry carried still arrives, and the handshake it + * started still completes. */ + XMEMSET(buf, 0, sizeof(buf)); + (void)test_tls13_early_data_read_until_write_ok(ssl_s, buf, sizeof(buf), + &earlyRead); + ExpectIntEQ(earlyRead, (int)sizeof(earlyMsg)); + ExpectStrEQ(earlyMsg, buf); + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + ExpectTrue(wolfSSL_session_reused(ssl_c)); + + /* Still the one record, over the whole handshake. */ + ExpectIntEQ(ccs_ctx.sent, 1); + + wolfSSL_SESSION_free(sess); + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} + int test_tls13_plaintext_alert(void) { EXPECT_DECLS; diff --git a/tests/api/test_tls13.h b/tests/api/test_tls13.h index 5474f33cac..2f9b5f4bc0 100644 --- a/tests/api/test_tls13.h +++ b/tests/api/test_tls13.h @@ -55,6 +55,13 @@ int test_key_share_mismatch(void); int test_key_share_mismatch_psk_dhe(void); int test_tls13_middlebox_compat_empty_session_id(void); int test_tls13_middlebox_compat_session_id(void); +int test_tls13_middlebox_compat_server_ccs(void); +int test_tls13_middlebox_compat_server_reuse(void); +int test_tls13_middlebox_compat_hrr_ccs(void); +int test_tls13_middlebox_compat_server_ccs_retry(void); +int test_tls13_middlebox_compat_hrr_ccs_retry(void); +int test_tls13_middlebox_compat_client_ccs_retry(void); +int test_tls13_middlebox_compat_client_ccs_retry_early_data(void); int test_tls13_plaintext_alert(void); int test_tls13_warning_alert_is_fatal(void); int test_tls13_unknown_ext_rejected(void); @@ -172,6 +179,14 @@ int test_tls13_cryptocb_async(void); TEST_DECL_GROUP("tls13", test_key_share_mismatch_psk_dhe), \ TEST_DECL_GROUP("tls13", test_tls13_middlebox_compat_empty_session_id), \ TEST_DECL_GROUP("tls13", test_tls13_middlebox_compat_session_id), \ + TEST_DECL_GROUP("tls13", test_tls13_middlebox_compat_server_ccs), \ + TEST_DECL_GROUP("tls13", test_tls13_middlebox_compat_server_reuse), \ + TEST_DECL_GROUP("tls13", test_tls13_middlebox_compat_hrr_ccs), \ + TEST_DECL_GROUP("tls13", test_tls13_middlebox_compat_server_ccs_retry), \ + TEST_DECL_GROUP("tls13", test_tls13_middlebox_compat_hrr_ccs_retry), \ + TEST_DECL_GROUP("tls13", test_tls13_middlebox_compat_client_ccs_retry), \ + TEST_DECL_GROUP("tls13", \ + test_tls13_middlebox_compat_client_ccs_retry_early_data), \ TEST_DECL_GROUP("tls13", test_tls13_plaintext_alert), \ TEST_DECL_GROUP("tls13", test_tls13_warning_alert_is_fatal), \ TEST_DECL_GROUP("tls13", test_tls13_cert_req_sigalgs), \ diff --git a/wolfssl/internal.h b/wolfssl/internal.h index f37d59292c..26d3bc92fe 100644 --- a/wolfssl/internal.h +++ b/wolfssl/internal.h @@ -5576,7 +5576,7 @@ struct Options { #ifdef WOLFSSL_ALT_CERT_CHAINS word16 usingAltCertChain:1;/* Alternate cert chain was used */ #endif -#if defined(WOLFSSL_TLS13) && defined(WOLFSSL_TLS13_MIDDLEBOX_COMPAT) +#ifdef WOLFSSL_TLS13 word16 sentChangeCipher:1; /* Change Cipher Spec sent */ #endif #if !defined(WOLFSSL_NO_CLIENT_AUTH) && \