diff --git a/components/esp-tls/esp_tls.h b/components/esp-tls/esp_tls.h index 7a4b8f3ca0..03801cd8bb 100644 --- a/components/esp-tls/esp_tls.h +++ b/components/esp-tls/esp_tls.h @@ -364,6 +364,12 @@ typedef struct esp_tls_cfg_server { Important note: the pointer must be valid for connection */ #endif + esp_tls_proto_ver_t tls_version; /*!< TLS protocol version for this server, e.g., TLS 1.2, TLS 1.3 + (default - no preference). Enables TLS version control per server instance. */ + + const int *ciphersuites_list; /*!< Pointer to a zero-terminated array of IANA identifiers of TLS ciphersuites. + Please check the list validity by esp_tls_get_ciphersuites_list() API. + This allows per-server cipher suite configuration. */ } esp_tls_cfg_server_t; /** diff --git a/components/esp-tls/esp_tls_mbedtls.c b/components/esp-tls/esp_tls_mbedtls.c index be9d82ba13..ec6137bc54 100644 --- a/components/esp-tls/esp_tls_mbedtls.c +++ b/components/esp-tls/esp_tls_mbedtls.c @@ -868,6 +868,29 @@ static esp_err_t set_server_config(esp_tls_cfg_server_t *cfg, esp_tls_t *tls) } #endif + // Configure per-service TLS version + const esp_tls_proto_ver_t tls_ver = cfg->tls_version; + if (tls_ver == ESP_TLS_VER_TLS_1_3) { +#if CONFIG_MBEDTLS_SSL_PROTO_TLS1_3 + ESP_LOGI(TAG, "Setting server TLS version to 0x%4x", MBEDTLS_SSL_VERSION_TLS1_3); + mbedtls_ssl_conf_min_tls_version(&tls->conf, MBEDTLS_SSL_VERSION_TLS1_3); + mbedtls_ssl_conf_max_tls_version(&tls->conf, MBEDTLS_SSL_VERSION_TLS1_3); +#else + ESP_LOGE(TAG, "TLS 1.3 is not enabled in config"); + return ESP_ERR_INVALID_ARG; +#endif + } else if (tls_ver == ESP_TLS_VER_TLS_1_2) { + ESP_LOGD(TAG, "Setting server TLS version to 0x%4x", MBEDTLS_SSL_VERSION_TLS1_2); + mbedtls_ssl_conf_min_tls_version(&tls->conf, MBEDTLS_SSL_VERSION_TLS1_2); + mbedtls_ssl_conf_max_tls_version(&tls->conf, MBEDTLS_SSL_VERSION_TLS1_2); + } + + if (cfg->ciphersuites_list != NULL && cfg->ciphersuites_list[0] != 0) { + ESP_LOGD(TAG, "Set the server ciphersuites list (user-provided)"); + mbedtls_ssl_conf_ciphersuites(&tls->conf, cfg->ciphersuites_list); + } else { + ESP_LOGD(TAG, "No custom cipher suites provided - using default"); + } return ESP_OK; } diff --git a/components/esp_https_server/include/esp_https_server.h b/components/esp_https_server/include/esp_https_server.h index e59052f075..c0154d0c70 100644 --- a/components/esp_https_server/include/esp_https_server.h +++ b/components/esp_https_server/include/esp_https_server.h @@ -141,6 +141,15 @@ struct httpd_ssl_config { /** TLS handshake timeout in milliseconds, default timeout is 10 seconds if not set */ uint32_t tls_handshake_timeout_ms; + + /** TLS protocol version for this server, e.g., TLS 1.2, TLS 1.3 + * (default - no preference). Enables per-server TLS version control. */ + esp_tls_proto_ver_t tls_version; + + /** Pointer to a zero-terminated array of IANA identifiers of TLS ciphersuites. + * Please check the list validity by esp_tls_get_ciphersuites_list() API. + * This allows per-server cipher suite configuration. */ + const int *ciphersuites_list; }; typedef struct httpd_ssl_config httpd_ssl_config_t; @@ -203,7 +212,9 @@ typedef struct httpd_ssl_config httpd_ssl_config_t; .ssl_userdata = NULL, \ .cert_select_cb = NULL, \ .alpn_protos = NULL, \ - .tls_handshake_timeout_ms = 0 \ + .tls_handshake_timeout_ms = 0, \ + .tls_version = ESP_TLS_VER_ANY, \ + .ciphersuites_list = NULL, \ } /** diff --git a/components/esp_https_server/src/https_server.c b/components/esp_https_server/src/https_server.c index 5ae7213072..5940bfa595 100644 --- a/components/esp_https_server/src/https_server.c +++ b/components/esp_https_server/src/https_server.c @@ -279,6 +279,9 @@ static esp_err_t create_secure_context(const struct httpd_ssl_config *config, ht cfg->alpn_protos = config->alpn_protos; cfg->tls_handshake_timeout_ms = config->tls_handshake_timeout_ms; + cfg->tls_version = config->tls_version; + cfg->ciphersuites_list = config->ciphersuites_list; + #if defined(CONFIG_ESP_HTTPS_SERVER_CERT_SELECT_HOOK) cfg->cert_select_cb = config->cert_select_cb; #endif diff --git a/examples/protocols/https_server/simple/main/Kconfig.projbuild b/examples/protocols/https_server/simple/main/Kconfig.projbuild index 23a891cd59..d3df06027a 100644 --- a/examples/protocols/https_server/simple/main/Kconfig.projbuild +++ b/examples/protocols/https_server/simple/main/Kconfig.projbuild @@ -7,4 +7,31 @@ menu "Example Configuration" Enable user callback for esp_https_server which can be used to get SSL context (connection information) E.g. Certificate of the connected client + choice EXAMPLE_ENABLE_HTTPS_SERVER_TLS_VERSION + prompt "Choose TLS version" + default EXAMPLE_ENABLE_HTTPS_SERVER_TLS_1_2_ONLY + config EXAMPLE_ENABLE_HTTPS_SERVER_TLS_1_2_ONLY + bool "TLS 1.2" + select MBEDTLS_SSL_PROTO_TLS1_2 + help + Enable HTTPS server TLS 1.2 + config EXAMPLE_ENABLE_HTTPS_SERVER_TLS_1_3_ONLY + bool "TLS 1.3" + select MBEDTLS_SSL_PROTO_TLS1_3 + help + Enable HTTPS server TLS 1.3 + config EXAMPLE_ENABLE_HTTPS_SERVER_TLS_ANY + bool "TLS 1.3 and 1.2" + select MBEDTLS_SSL_PROTO_TLS1_3 + select MBEDTLS_SSL_PROTO_TLS1_2 + help + Enable HTTPS server TLS 1.3 and 1.2 + endchoice + + config EXAMPLE_ENABLE_HTTPS_SERVER_CUSTOM_CIPHERSUITES + bool "Enable HTTPS server custom ciphersuites" + default n + help + Enable HTTPS server custom ciphersuites + endmenu diff --git a/examples/protocols/https_server/simple/main/main.c b/examples/protocols/https_server/simple/main/main.c index 7c58891e9a..9dbcd9b9f0 100644 --- a/examples/protocols/https_server/simple/main/main.c +++ b/examples/protocols/https_server/simple/main/main.c @@ -21,6 +21,10 @@ #include "esp_tls.h" #include "sdkconfig.h" +#if CONFIG_EXAMPLE_ENABLE_HTTPS_SERVER_CUSTOM_CIPHERSUITES +#include "mbedtls/ssl_ciphersuites.h" +#endif // CONFIG_EXAMPLE_ENABLE_HTTPS_SERVER_CUSTOM_CIPHERSUITES + /* A simple example that demonstrates how to create GET and POST * handlers and start an HTTPS server. */ @@ -159,6 +163,27 @@ static httpd_handle_t start_webserver(void) conf.prvtkey_pem = prvtkey_pem_start; conf.prvtkey_len = prvtkey_pem_end - prvtkey_pem_start; +#if CONFIG_EXAMPLE_ENABLE_HTTPS_SERVER_CUSTOM_CIPHERSUITES + static const int ciphersuites_to_use[] = { + MBEDTLS_TLS_DHE_RSA_WITH_AES_128_CBC_SHA256, + MBEDTLS_TLS_DHE_RSA_WITH_AES_256_CBC_SHA256, + MBEDTLS_TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384, + MBEDTLS_TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, + 0, + }; + conf.ciphersuites_list = ciphersuites_to_use; +#else + conf.ciphersuites_list = NULL; +#endif // CONFIG_EXAMPLE_ENABLE_HTTPS_SERVER_CUSTOM_CIPHERSUITES + +#if CONFIG_EXAMPLE_ENABLE_HTTPS_SERVER_TLS_1_3_ONLY + conf.tls_version = ESP_TLS_VER_TLS_1_3; +#elif CONFIG_EXAMPLE_ENABLE_HTTPS_SERVER_TLS_1_2_ONLY + conf.tls_version = ESP_TLS_VER_TLS_1_2; +#else + conf.tls_version = ESP_TLS_VER_ANY; +#endif // CONFIG_EXAMPLE_ENABLE_HTTPS_SERVER_TLS_1_3_ONLY + #if CONFIG_EXAMPLE_ENABLE_HTTPS_USER_CALLBACK conf.user_cb = https_server_user_callback; #endif diff --git a/examples/protocols/https_server/simple/pytest_https_server_simple.py b/examples/protocols/https_server/simple/pytest_https_server_simple.py index 216b4fb642..49f045c7d2 100644 --- a/examples/protocols/https_server/simple/pytest_https_server_simple.py +++ b/examples/protocols/https_server/simple/pytest_https_server_simple.py @@ -109,7 +109,7 @@ def test_examples_protocol_https_server_simple(dut: Dut) -> None: # check and log bin size binary_file = os.path.join(dut.app.binary_path, 'https_server.bin') bin_size = os.path.getsize(binary_file) - logging.info('https_server_simple_bin_size : {}KB'.format(bin_size // 1024)) + logging.info(f'https_server_simple_bin_size : {bin_size // 1024}KB') # start test logging.info('Waiting to connect with AP') if dut.app.sdkconfig.get('EXAMPLE_WIFI_SSID_PWD_FROM_STDIN') is True: @@ -125,8 +125,8 @@ def test_examples_protocol_https_server_simple(dut: Dut) -> None: # Expected logs - logging.info('Got IP : {}'.format(got_ip)) - logging.info('Got Port : {}'.format(got_port)) + logging.info(f'Got IP : {got_ip}') + logging.info(f'Got Port : {got_port}') logging.info('Performing GET request over an SSL connection with the server') @@ -156,7 +156,7 @@ def test_examples_protocol_https_server_simple(dut: Dut) -> None: if dut.app.sdkconfig.get('CONFIG_EXAMPLE_ENABLE_HTTPS_USER_CALLBACK') is True: current_cipher = dut.expect(r'Current Ciphersuite(.*)', timeout=5)[0] - logging.info('Current Ciphersuite {}'.format(current_cipher)) + logging.info(f'Current Ciphersuite {current_cipher}') logging.info('Checking user callback: Obtaining client certificate...') @@ -166,9 +166,9 @@ def test_examples_protocol_https_server_simple(dut: Dut) -> None: 1 ].decode() - logging.info('Serial No. {}'.format(serial_number)) - logging.info('Issuer Name {}'.format(issuer_name)) - logging.info('Expires on {}'.format(expiry)) + logging.info(f'Serial No. {serial_number}') + logging.info(f'Issuer Name {issuer_name}') + logging.info(f'Expires on {expiry}') # Close the connection conn.close() @@ -203,8 +203,8 @@ def test_examples_protocol_https_server_simple_dynamic_buffers(dut: Dut) -> None # Expected logs - logging.info('Got IP : {}'.format(got_ip)) - logging.info('Got Port : {}'.format(got_port)) + logging.info(f'Got IP : {got_ip}') + logging.info(f'Got Port : {got_port}') logging.info('Performing GET request over an SSL connection with the server') @@ -233,7 +233,7 @@ def test_examples_protocol_https_server_simple_dynamic_buffers(dut: Dut) -> None if dut.app.sdkconfig.get('CONFIG_EXAMPLE_ENABLE_HTTPS_USER_CALLBACK') is True: current_cipher = dut.expect(r'Current Ciphersuite(.*)', timeout=5)[0] - logging.info('Current Ciphersuite {}'.format(current_cipher)) + logging.info(f'Current Ciphersuite {current_cipher}') logging.info('Checking user callback: Obtaining client certificate...') @@ -243,9 +243,9 @@ def test_examples_protocol_https_server_simple_dynamic_buffers(dut: Dut) -> None 1 ].decode() - logging.info('Serial No. : {}'.format(serial_number)) - logging.info('Issuer Name : {}'.format(issuer_name)) - logging.info('Expires on : {}'.format(expiry)) + logging.info(f'Serial No. : {serial_number}') + logging.info(f'Issuer Name : {issuer_name}') + logging.info(f'Expires on : {expiry}') # Close the connection conn.close() @@ -277,8 +277,8 @@ def test_examples_protocol_https_server_tls1_3(dut: Dut) -> None: # Expected logs - logging.info('Got IP : {}'.format(got_ip)) - logging.info('Got Port : {}'.format(got_port)) + logging.info(f'Got IP : {got_ip}') + logging.info(f'Got Port : {got_port}') logging.info('Performing GET request over an SSL connection with the server using TLSv1.3') CLIENT_CERT_FILE = 'client_cert.pem' @@ -288,14 +288,25 @@ def test_examples_protocol_https_server_tls1_3(dut: Dut) -> None: cert.write(client_cert_pem) key.write(client_key_pem) + # First try with TLSv1.2 and that should fail ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) - ssl_context.minimum_version = ssl.TLSVersion.TLSv1_3 - ssl_context.maximum_version = ssl.TLSVersion.TLSv1_3 + ssl_context.minimum_version = ssl.TLSVersion.TLSv1_2 + ssl_context.maximum_version = ssl.TLSVersion.TLSv1_2 ssl_context.verify_mode = ssl.CERT_REQUIRED ssl_context.check_hostname = False ssl_context.load_verify_locations(cadata=server_cert_pem) - ssl_context.load_cert_chain(certfile=CLIENT_CERT_FILE, keyfile=CLIENT_KEY_FILE) + conn = http.client.HTTPSConnection(got_ip, got_port, context=ssl_context) + try: + conn.request('GET', '/') + except ssl.SSLError as e: + logging.info(f'SSL handshake failed with TLSv1.2: {e}') + else: + logging.info('SSL handshake succeeded with TLSv1.2') + raise RuntimeError('This should have failed') + + ssl_context.minimum_version = ssl.TLSVersion.TLSv1_3 + ssl_context.maximum_version = ssl.TLSVersion.TLSv1_3 os.remove(CLIENT_CERT_FILE) os.remove(CLIENT_KEY_FILE) @@ -312,7 +323,7 @@ def test_examples_protocol_https_server_tls1_3(dut: Dut) -> None: if dut.app.sdkconfig.get('CONFIG_EXAMPLE_ENABLE_HTTPS_USER_CALLBACK') is True: current_cipher = dut.expect(r'Current Ciphersuite(.*)', timeout=5)[0] - logging.info('Current Ciphersuite {}'.format(current_cipher)) + logging.info(f'Current Ciphersuite {current_cipher}') logging.info('Checking user callback: Obtaining client certificate...') @@ -323,9 +334,120 @@ def test_examples_protocol_https_server_tls1_3(dut: Dut) -> None: timeout=5, )[1].decode() - logging.info('Serial No. : {}'.format(serial_number)) - logging.info('Issuer Name : {}'.format(issuer_name)) - logging.info('Expires on : {}'.format(expiry)) + logging.info(f'Serial No. : {serial_number}') + logging.info(f'Issuer Name : {issuer_name}') + logging.info(f'Expires on : {expiry}') + + # Close the connection + conn.close() + logging.info('Correct response obtained') + logging.info('SSL connection test successful\nClosing the connection') + + +@pytest.mark.wifi_router +@pytest.mark.parametrize( + 'config', + [ + 'tls1_2_only', + ], + indirect=True, +) +@idf_parametrize('target', ['esp32', 'esp32c3', 'esp32s3'], indirect=['target']) +def test_examples_protocol_https_server_tls1_2_only(dut: Dut) -> None: + logging.info('Waiting to connect with AP') + if dut.app.sdkconfig.get('EXAMPLE_WIFI_SSID_PWD_FROM_STDIN') is True: + dut.expect('Please input ssid password:') + env_name = 'wifi_router' + ap_ssid = get_env_config_variable(env_name, 'ap_ssid') + ap_password = get_env_config_variable(env_name, 'ap_password') + dut.write(f'{ap_ssid} {ap_password}') + # Parse IP address and port of the server + dut.expect(r'Starting server') + got_port = int(dut.expect(r'Server listening on port (\d+)', timeout=30)[1].decode()) + got_ip = dut.expect(r'IPv4 address: (\d+\.\d+\.\d+\.\d+)[^\d]', timeout=30)[1].decode() + + # Expected logs + logging.info(f'Got IP : {got_ip}') + logging.info(f'Got Port : {got_port}') + logging.info('Performing GET request over an SSL connection with the server using TLSv1.2') + + CLIENT_CERT_FILE = 'client_cert.pem' + CLIENT_KEY_FILE = 'client_key.pem' + + with open(CLIENT_CERT_FILE, 'w', encoding='utf-8') as cert, open(CLIENT_KEY_FILE, 'w', encoding='utf-8') as key: + cert.write(client_cert_pem) + key.write(client_key_pem) + + # First try with TLSv1.3 and that should fail + ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + ssl_context.minimum_version = ssl.TLSVersion.TLSv1_3 + ssl_context.maximum_version = ssl.TLSVersion.TLSv1_3 + ssl_context.verify_mode = ssl.CERT_REQUIRED + ssl_context.check_hostname = False + ssl_context.load_verify_locations(cadata=server_cert_pem) + ssl_context.load_cert_chain(certfile=CLIENT_CERT_FILE, keyfile=CLIENT_KEY_FILE) + conn = http.client.HTTPSConnection(got_ip, got_port, context=ssl_context) + try: + conn.request('GET', '/') + except ssl.SSLError as e: + logging.info(f'SSL handshake failed with TLSv1.3: {e}') + else: + logging.info('SSL handshake succeeded with TLSv1.3') + raise RuntimeError('This should have failed') + + ssl_context.minimum_version = ssl.TLSVersion.TLSv1_2 + ssl_context.maximum_version = ssl.TLSVersion.TLSv1_2 + + # Also now with TLS1.2, try with a non matching ciphersuite and that should fail + # Server only accepts: DHE-RSA-AES128-SHA256, DHE-RSA-AES256-SHA256, + # ECDHE-RSA-AES256-SHA384, ECDHE-RSA-AES128-SHA256 + # Try AES128-GCM-SHA256 which is NOT in the list + ssl_context.set_ciphers('AES128-GCM-SHA256') + + conn = http.client.HTTPSConnection(got_ip, got_port, context=ssl_context) + try: + logging.info('Trying SSL handshake with non-matching ciphersuite (should fail)') + conn.request('GET', '/') + except ssl.SSLError as e: + logging.info(f'SSL handshake failed with non-matching ciphersuite (expected): {e}') + else: + logging.info('SSL handshake succeeded with non-matching ciphersuite') + raise RuntimeError('This should have failed - custom ciphersuites not enforced') + finally: + conn.close() + + # Now try with the matching ciphersuite + ssl_context.set_ciphers('DHE-RSA-AES128-SHA256') + + os.remove(CLIENT_CERT_FILE) + os.remove(CLIENT_KEY_FILE) + + conn = http.client.HTTPSConnection(got_ip, got_port, context=ssl_context) + logging.info('Performing SSL handshake with the server') + conn.request('GET', '/') + resp = conn.getresponse() + dut.expect('performing session handshake') + got_resp = resp.read().decode('utf-8') + if got_resp != success_response: + logging.info('Response obtained does not match with correct response') + raise RuntimeError('Failed to test SSL connection') + + if dut.app.sdkconfig.get('CONFIG_EXAMPLE_ENABLE_HTTPS_USER_CALLBACK') is True: + current_cipher = dut.expect(r'Current Ciphersuite(.*)', timeout=5)[0] + logging.info(f'Current Ciphersuite {current_cipher}') + + logging.info('Checking user callback: Obtaining client certificate...') + + serial_number = dut.expect(r'serial number\s*:([^\n]*)', timeout=5)[0] + issuer_name = dut.expect(r'issuer name\s*:([^\n]*)', timeout=5)[0] + expiry = dut.expect( + r'expires on\s*:((.*)\d{4}\-(0?[1-9]|1[012])\-(0?[1-9]|[12][0-9]|3[01])*)', + timeout=5, + )[1].decode() + + logging.info(f'Serial No. : {serial_number}') + logging.info(f'Issuer Name : {issuer_name}') + logging.info(f'Expires on : {expiry}') # Close the connection conn.close() diff --git a/examples/protocols/https_server/simple/sdkconfig.ci.tls1_2_only b/examples/protocols/https_server/simple/sdkconfig.ci.tls1_2_only new file mode 100644 index 0000000000..2dc708b7f4 --- /dev/null +++ b/examples/protocols/https_server/simple/sdkconfig.ci.tls1_2_only @@ -0,0 +1,5 @@ +CONFIG_ESP_HTTPS_SERVER_ENABLE=y +CONFIG_ESP_HTTPS_SERVER_CERT_SELECT_HOOK=y +CONFIG_EXAMPLE_WIFI_SSID_PWD_FROM_STDIN=y +CONFIG_EXAMPLE_ENABLE_HTTPS_SERVER_TLS_1_2_ONLY=y +CONFIG_EXAMPLE_ENABLE_HTTPS_SERVER_CUSTOM_CIPHERSUITES=y diff --git a/examples/protocols/https_server/simple/sdkconfig.ci.tls1_3 b/examples/protocols/https_server/simple/sdkconfig.ci.tls1_3 index 8942911dd6..2f9b090741 100644 --- a/examples/protocols/https_server/simple/sdkconfig.ci.tls1_3 +++ b/examples/protocols/https_server/simple/sdkconfig.ci.tls1_3 @@ -6,3 +6,4 @@ CONFIG_MBEDTLS_SSL_TLS1_3_COMPATIBILITY_MODE=y CONFIG_MBEDTLS_SSL_TLS1_3_KEXM_PSK=y CONFIG_MBEDTLS_SSL_TLS1_3_KEXM_EPHEMERAL=y CONFIG_MBEDTLS_SSL_TLS1_3_KEXM_PSK_EPHEMERAL=y +CONFIG_EXAMPLE_ENABLE_HTTPS_SERVER_TLS_1_3_ONLY=y