diff --git a/.github/scripts/openssl-ech.sh b/.github/scripts/openssl-ech.sh index ca669de450..fd61f1d2f0 100644 --- a/.github/scripts/openssl-ech.sh +++ b/.github/scripts/openssl-ech.sh @@ -11,7 +11,7 @@ cleanup() { trap cleanup EXIT usage() { - echo "Usage: $0 [--suite ] [--pqc ] [--hrr] [--workspace ]" + echo "Usage: $0 [--suite ] [--pqc ] [--hrr] [--reject] [--workspace ]" exit 1 } @@ -22,6 +22,7 @@ MODE="" SUITE="" PQC="" FORCE_HRR=0 +REJECT=0 WORKSPACE=${GITHUB_WORKSPACE:-"."} @@ -51,6 +52,10 @@ while [ $# -gt 0 ]; do FORCE_HRR=1 shift ;; + --reject) + REJECT=1 + shift + ;; --workspace) [ -z "$2" ] && { echo "ERROR: --workspace requires a value"; exit 1; } WORKSPACE="$2" @@ -84,9 +89,12 @@ WOLFSSL_CLIENT=${WOLFSSL_CLIENT:-"$WORKSPACE/examples/client/client"} WOLFSSL_SERVER=${WOLFSSL_SERVER:-"$WORKSPACE/examples/server/server"} CERT_DIR=${CERT_DIR:-"$WORKSPACE/certs"} +# correct ECH config, but it's old, ECH will be rejected +REJECT_ECH_CONFIG="AD7+DQA6rAAgACCATZdDlHed6GlDeiYsu3r7sdWUkLVHZuTa3lbOf+hIbAAEAAEAAQALZXhhbXBsZS5jb20AAA==" + TMP_LOG="$WORKSPACE/tmp_file.log" PRIV_NAME="ech-private-name.com" -PUB_NAME="ech-public-name.com" +PUB_NAME="example.com" MAX_WAIT=50 # -------------------------------------------------------------------------- @@ -128,6 +136,8 @@ openssl_server(){ # parse ECH config from file ech_config=$(sed -n '/BEGIN ECHCONFIG/,/END ECHCONFIG/{/BEGIN ECHCONFIG\|END ECHCONFIG/d;p}' "$ech_file" | tr -d '\n') + # reject overrides the config the client connects with + [ "$REJECT" -ne 0 ] && ech_config="$REJECT_ECH_CONFIG" echo "parsed ech config: $ech_config" &>> "$TMP_LOG" # start OpenSSL ECH server with ephemeral port; line-buffer so the @@ -158,17 +168,29 @@ openssl_server(){ done echo "parsed port: $port" &>> "$TMP_LOG" - # test with wolfssl client - $WOLFSSL_CLIENT -v 4 \ - -p "$port" \ - -S "$PRIV_NAME" \ - --ech "$ech_config" \ - $wolfssl_extra \ - &>> "$TMP_LOG" - rm -f "$ech_file" - grep -q "ech_success=1" "$TMP_LOG" + # test with wolfssl client + if [ "$REJECT" -ne 0 ]; then + $WOLFSSL_CLIENT -v 4 \ + -p "$port" \ + -S "$PRIV_NAME" \ + --ech "$ech_config" \ + $wolfssl_extra \ + &>> "$TMP_LOG" || true + + grep -q "ECH offered but rejected by server" "$TMP_LOG" + grep -q "ech_success=0" "$TMP_LOG" + else + $WOLFSSL_CLIENT -v 4 \ + -p "$port" \ + -S "$PRIV_NAME" \ + --ech "$ech_config" \ + $wolfssl_extra \ + &>> "$TMP_LOG" + + grep -q "ech_success=1" "$TMP_LOG" + fi } # -------------------------------------------------------------------------- @@ -246,21 +268,39 @@ openssl_client(){ exit 1 fi done + # reject overrides the config the client connects with + [ "$REJECT" -ne 0 ] && ech_config="$REJECT_ECH_CONFIG" echo "parsed ech config: $ech_config" &>> "$TMP_LOG" - # test with OpenSSL s_client using ECH - echo "wolfssl" | $OPENSSL s_client \ - -tls1_3 \ - -connect "localhost:$port" \ - -cert "$CERT_DIR/client-cert.pem" \ - -key "$CERT_DIR/client-key.pem" \ - -CAfile "$CERT_DIR/ca-cert.pem" \ - -servername "$PRIV_NAME" \ - -ech_config_list "$ech_config" \ - $openssl_groups \ - &>> "$TMP_LOG" - - grep -q "ECH: success: 1" "$TMP_LOG" + if [ "$REJECT" -ne 0 ]; then + # test with OpenSSL s_client using ECH + echo "wolfssl" | $OPENSSL s_client \ + -tls1_3 \ + -connect "localhost:$port" \ + -cert "$CERT_DIR/client-cert.pem" \ + -key "$CERT_DIR/client-key.pem" \ + -CAfile "$CERT_DIR/ca-cert.pem" \ + -servername "$PRIV_NAME" \ + -ech_config_list "$ech_config" \ + $openssl_groups \ + &>> "$TMP_LOG" || true + + grep -q "ECH: Got 1 retry-configs" "$TMP_LOG" + else + # test with OpenSSL s_client using ECH + echo "wolfssl" | $OPENSSL s_client \ + -tls1_3 \ + -connect "localhost:$port" \ + -cert "$CERT_DIR/client-cert.pem" \ + -key "$CERT_DIR/client-key.pem" \ + -CAfile "$CERT_DIR/ca-cert.pem" \ + -servername "$PRIV_NAME" \ + -ech_config_list "$ech_config" \ + $openssl_groups \ + &>> "$TMP_LOG" + + grep -q "ECH: success: 1" "$TMP_LOG" + fi } rm -f "$TMP_LOG" diff --git a/.github/workflows/openssl-ech.yml b/.github/workflows/openssl-ech.yml index 4d3ae03e69..3332165370 100644 --- a/.github/workflows/openssl-ech.yml +++ b/.github/workflows/openssl-ech.yml @@ -167,6 +167,12 @@ jobs: echo -e "\nTesting weird suite with OpenSSL client and wolfSSL server\n" &>> "$LOG_FILE" bash ./openssl-ech.sh client --suite "18,1,2" &>> "$LOG_FILE" + echo -e "\nTesting rejection with OpenSSL server and wolfSSL client\n" &>> "$LOG_FILE" + bash ./openssl-ech.sh server --reject &>> "$LOG_FILE" + + echo -e "\nTesting rejection with OpenSSL client and wolfSSL server\n" &>> "$LOG_FILE" + bash ./openssl-ech.sh client --reject &>> "$LOG_FILE" + # cleanup rm -f "$LOG_FILE" diff --git a/tests/api.c b/tests/api.c index 6208cc2b2d..09ac8be8b5 100644 --- a/tests/api.c +++ b/tests/api.c @@ -14024,6 +14024,63 @@ static word16 echCbTestKemID = 0; static word16 echCbTestKdfID = 0; static word16 echCbTestAeadID = 0; +/* return the index of the first extension + * extLen will be updated with the length of the extensions field */ +static int ech_seek_extensions(byte* buf, word16* extLen) +{ + word16 idx; + byte sessionIdLen; + word16 cipherSuitesLen; + byte compressionLen; + + idx = OPAQUE16_LEN + RAN_LEN; + + sessionIdLen = buf[idx++]; + idx += sessionIdLen; + + ato16(buf + idx, &cipherSuitesLen); + idx += OPAQUE16_LEN + cipherSuitesLen; + + compressionLen = buf[idx++]; + idx += compressionLen; + + ato16(buf + idx, extLen); + idx += OPAQUE16_LEN; + + return idx; +} + +/* locate a particular extension: + * idx_p is updated with the location of that extension + * -> idx_p should start just after the handshake header + * 0 returned on success, error otherwise */ +static int ech_find_extension(byte* buf, word16* idx_p, word16 extType) +{ + word16 idx; + word16 extIdx; + word16 extLen; + + extIdx = ech_seek_extensions(buf + *idx_p, &extLen) + *idx_p; + idx = extIdx; + + while (idx - extIdx < extLen) { + word16 type; + word16 len; + + ato16(buf + idx, &type); + if (type == extType) { + *idx_p = idx; + return 0; + } + + idx += OPAQUE16_LEN; + ato16(buf + idx, &len); + idx += OPAQUE16_LEN + len; + } + + return BAD_FUNC_ARG; +} + /* the arg is whether the client has ech enabled or not */ static int test_ech_server_sni_callback(WOLFSSL* ssl, int* ad, void* arg) { @@ -14895,6 +14952,124 @@ static int test_wolfSSL_Tls13_ECH_GREASE(void) return EXPECT_RESULT(); } +/* The public name must be visible and the private name must not be visible */ +static int test_wolfSSL_Tls13_ECH_wire_sni_ex(int accept) +{ + EXPECT_DECLS; + test_ssl_memio_ctx test_ctx; + word16 idx; + word16 nameLen; + word16 publicLen = (word16)XSTRLEN(echCbTestPublicName); + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + + test_ctx.s_cb.method = wolfTLSv1_3_server_method; + test_ctx.c_cb.method = wolfTLSv1_3_client_method; + + test_ctx.s_cb.ctx_ready = test_ech_server_ctx_ready; + test_ctx.s_cb.ssl_ready = test_ech_server_ssl_ready; + /* Accept path uses the correct configs */ + if (accept) + test_ctx.c_cb.ssl_ready = test_ech_client_ssl_ready; + + ExpectIntEQ(test_ssl_memio_setup(&test_ctx), TEST_SUCCESS); + + /* Reject path installs bad configs (with the correct public name) */ + if (!accept) { + WOLFSSL_CTX* tempCtx = NULL; + byte badConfig[128]; + word32 badConfigLen = sizeof(badConfig); + + ExpectNotNull(tempCtx = wolfSSL_CTX_new(wolfTLSv1_3_server_method())); + ExpectIntEQ(wolfSSL_CTX_GenerateEchConfig(tempCtx, echCbTestPublicName, + 0, 0, 0), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_GetEchConfigs(tempCtx, badConfig, + &badConfigLen), WOLFSSL_SUCCESS); + wolfSSL_CTX_free(tempCtx); + ExpectIntEQ(wolfSSL_SetEchConfigs(test_ctx.c_ssl, badConfig, + badConfigLen), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_UseSNI(test_ctx.c_ssl, WOLFSSL_SNI_HOST_NAME, + echCbTestPrivateName, (word16)XSTRLEN(echCbTestPrivateName)), + WOLFSSL_SUCCESS); + } + + /* force HelloRetryRequest */ + ExpectIntEQ(wolfSSL_NoKeyShares(test_ctx.c_ssl), WOLFSSL_SUCCESS); + + /* On reject, client aborts with ech_required and won't send a cert. */ + if (!accept) { + wolfSSL_set_verify(test_ctx.s_ssl, WOLFSSL_VERIFY_NONE, NULL); + wolfSSL_set_verify(test_ctx.c_ssl, WOLFSSL_VERIFY_PEER, NULL); + } + + /* client writes CH1 into s_buff */ + ExpectIntEQ(wolfSSL_connect(test_ctx.c_ssl), WOLFSSL_FATAL_ERROR); + ExpectIntEQ(wolfSSL_get_error(test_ctx.c_ssl, WOLFSSL_FATAL_ERROR), + WOLFSSL_ERROR_WANT_READ); + + /* check sent SNI is correct */ + idx = RECORD_HEADER_SZ + HANDSHAKE_HEADER_SZ; + ExpectIntEQ(ech_find_extension(test_ctx.s_buff, &idx, TLSXT_SERVER_NAME), + 0); + ExpectIntEQ(test_ctx.s_buff[idx + 6], WOLFSSL_SNI_HOST_NAME); + ato16(test_ctx.s_buff + idx + 7, &nameLen); + ExpectIntEQ(nameLen, publicLen); + ExpectIntEQ(XMEMCMP(test_ctx.s_buff + idx + 9, echCbTestPublicName, + publicLen), 0); + + /* server consumes CH1 and writes HRR into c_buff */ + ExpectIntEQ(wolfSSL_accept(test_ctx.s_ssl), WOLFSSL_FATAL_ERROR); + ExpectIntEQ(wolfSSL_get_error(test_ctx.s_ssl, WOLFSSL_FATAL_ERROR), + WOLFSSL_ERROR_WANT_READ); + ExpectIntEQ(test_ctx.s_ssl->options.serverState, + SERVER_HELLO_RETRY_REQUEST_COMPLETE); + + /* client reads HRR from c_buff and writes CH2 into s_buff */ + ExpectIntEQ(wolfSSL_connect(test_ctx.c_ssl), WOLFSSL_FATAL_ERROR); + ExpectIntEQ(wolfSSL_get_error(test_ctx.c_ssl, WOLFSSL_FATAL_ERROR), + WOLFSSL_ERROR_WANT_READ); + + /* check sent SNI is correct */ + idx = RECORD_HEADER_SZ + HANDSHAKE_HEADER_SZ; + ExpectIntEQ(ech_find_extension(test_ctx.s_buff, &idx, TLSXT_SERVER_NAME), + 0); + ExpectIntEQ(test_ctx.s_buff[idx + 6], WOLFSSL_SNI_HOST_NAME); + ato16(test_ctx.s_buff + idx + 7, &nameLen); + ExpectIntEQ(nameLen, publicLen); + ExpectIntEQ(XMEMCMP(test_ctx.s_buff + idx + 9, echCbTestPublicName, + publicLen), 0); + + /* sanity check: finish the handshake and verify ECH acceptance */ + if (accept) { + ExpectIntEQ(test_ssl_memio_do_handshake(&test_ctx, 10, NULL), + TEST_SUCCESS); + ExpectIntEQ(wolfSSL_GetEchStatus(test_ctx.c_ssl), + WOLFSSL_ECH_STATUS_ACCEPTED); + ExpectIntEQ(wolfSSL_GetEchStatus(test_ctx.s_ssl), + WOLFSSL_ECH_STATUS_ACCEPTED); + } + else { + ExpectIntNE(test_ssl_memio_do_handshake(&test_ctx, 10, NULL), + TEST_SUCCESS); + ExpectIntEQ(wolfSSL_GetEchStatus(test_ctx.c_ssl), + WOLFSSL_ECH_STATUS_REJECTED); + ExpectIntEQ(wolfSSL_GetEchStatus(test_ctx.s_ssl), + WOLFSSL_ECH_STATUS_REJECTED); + } + + test_ssl_memio_cleanup(&test_ctx); + + return EXPECT_RESULT(); +} + +static int test_wolfSSL_Tls13_ECH_wire_sni(void) +{ + EXPECT_DECLS; + ExpectIntEQ(test_wolfSSL_Tls13_ECH_wire_sni_ex(0), TEST_SUCCESS); + ExpectIntEQ(test_wolfSSL_Tls13_ECH_wire_sni_ex(1), TEST_SUCCESS); + return EXPECT_RESULT(); +} + static int test_wolfSSL_Tls13_ECH_disable_conn_ex(int enableServer, int enableClient) { @@ -15008,57 +15183,6 @@ static int test_wolfSSL_Tls13_ECH_long_SNI(void) return EXPECT_RESULT(); } -static int ech_seek_extensions(byte* buf, word16* innerExtLen) -{ - word16 idx; - byte sessionIdLen; - word16 cipherSuitesLen; - byte compressionLen; - - idx = OPAQUE16_LEN + RAN_LEN; - - sessionIdLen = buf[idx++]; - idx += sessionIdLen; - - ato16(buf + idx, &cipherSuitesLen); - idx += OPAQUE16_LEN + cipherSuitesLen; - - compressionLen = buf[idx++]; - idx += compressionLen; - - ato16(buf + idx, innerExtLen); - idx += OPAQUE16_LEN; - - return idx; -} - -static int ech_find_extension(byte* buf, word16* idx_p, word16 extType) -{ - word16 idx; - word16 innerExtIdx; - word16 innerExtLen; - - innerExtIdx = ech_seek_extensions(buf + *idx_p, &innerExtLen) + *idx_p; - idx = innerExtIdx; - - while (idx - innerExtIdx < innerExtLen) { - word16 type; - word16 len; - - ato16(buf + idx, &type); - if (type == extType) { - *idx_p = idx; - return 0; - } - - idx += OPAQUE16_LEN; - ato16(buf + idx, &len); - idx += OPAQUE16_LEN + len; - } - - return BAD_FUNC_ARG; -} - /* Test the HRR ECH rejection fallback path: * client offers ECH, HRR is triggered, server sends HRR without ECH extension, * client falls back to the outer transcript, then aborts with ech_required. @@ -35111,6 +35235,7 @@ TEST_CASE testCases[] = { TEST_DECL(test_wolfSSL_Tls13_ECH_new_config), TEST_DECL(test_wolfSSL_Tls13_ECH_trial_decrypt), TEST_DECL(test_wolfSSL_Tls13_ECH_GREASE), + TEST_DECL(test_wolfSSL_Tls13_ECH_wire_sni), TEST_DECL(test_wolfSSL_Tls13_ECH_disable_conn), TEST_DECL(test_wolfSSL_Tls13_ECH_long_SNI), TEST_DECL(test_wolfSSL_Tls13_ECH_HRR_rejection),