From 0d8ff68f8a47fc05a6b1d3a5bda24764a48e5fbd Mon Sep 17 00:00:00 2001 From: Ashish Sharma Date: Mon, 27 Apr 2026 11:18:01 +0800 Subject: [PATCH 1/3] fix(esp_crt_bundle): fixes a potential memory leak with cross signed certificates Closes https://github.com/espressif/esp-idf/issues/18512 Closes https://github.com/espressif/esp-idf/issues/18550 --- .../mbedtls/esp_crt_bundle/esp_crt_bundle.c | 69 ++++++++----------- 1 file changed, 29 insertions(+), 40 deletions(-) diff --git a/components/mbedtls/esp_crt_bundle/esp_crt_bundle.c b/components/mbedtls/esp_crt_bundle/esp_crt_bundle.c index 05eff00b9cb..48339ed214b 100644 --- a/components/mbedtls/esp_crt_bundle/esp_crt_bundle.c +++ b/components/mbedtls/esp_crt_bundle/esp_crt_bundle.c @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2018-2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2018-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -429,7 +429,10 @@ static esp_err_t esp_crt_bundle_init(const uint8_t* const x509_bundle, const siz } #if defined(CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_CROSS_SIGNED_VERIFY) -static int esp_crt_copy_asn1(const mbedtls_asn1_named_data *src, mbedtls_asn1_named_data *dst) +/* Reference ASN1 named data by pointing into src's buffers. + * The src data must outlive dst. */ +static int esp_crt_ref_asn1(const mbedtls_asn1_named_data *src, + mbedtls_asn1_named_data *dst) { if (src == NULL || dst == NULL) { return -1; @@ -437,21 +440,11 @@ static int esp_crt_copy_asn1(const mbedtls_asn1_named_data *src, mbedtls_asn1_na dst->oid.tag = src->oid.tag; dst->oid.len = src->oid.len; - dst->oid.p = calloc(1, src->oid.len); - if (dst->oid.p == NULL) { - ESP_LOGE(TAG, "Failed to allocate memory for OID"); - return -1; - } - memcpy(dst->oid.p, src->oid.p, src->oid.len); + dst->oid.p = src->oid.p; dst->val.tag = src->val.tag; dst->val.len = src->val.len; - dst->val.p = calloc(1, src->val.len); - if (dst->val.p == NULL) { - ESP_LOGE(TAG, "Failed to allocate memory for value"); - free(dst->oid.p); - return -1; - } - memcpy(dst->val.p, src->val.p, src->val.len); + dst->val.p = src->val.p; + dst->next_merged = src->next_merged; return 0; } @@ -484,16 +477,10 @@ static int esp_crt_ca_cb_callback(void *ctx, mbedtls_x509_crt const *child, mbed const uint8_t *cert_name = esp_crt_get_name(cert); uint16_t cert_name_len = esp_crt_get_name_len(cert); + /* Point into persistent bundle data */ new_cert->subject_raw.tag = MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE; new_cert->subject_raw.len = cert_name_len; - new_cert->subject_raw.p = calloc(1, cert_name_len); - if (new_cert->subject_raw.p == NULL) { - ESP_LOGE(TAG, "Failed to allocate memory for subject"); - mbedtls_x509_crt_free(new_cert); - free(new_cert); - return MBEDTLS_ERR_X509_ALLOC_FAILED; - } - memcpy(new_cert->subject_raw.p, cert_name, cert_name_len); + new_cert->subject_raw.p = (unsigned char *)cert_name; const uint8_t *cert_key = esp_crt_get_key(cert); uint16_t cert_key_len = esp_crt_get_key_len(cert); @@ -508,31 +495,33 @@ static int esp_crt_ca_cb_callback(void *ctx, mbedtls_x509_crt const *child, mbed return ret; } - // Loop through the child->issuer and copy the values to the new certificate + /* Populate parent->subject by referencing child->issuer data */ const mbedtls_asn1_named_data *child_issuer = &child->issuer; mbedtls_asn1_named_data *parent_subject = &new_cert->subject; + if (esp_crt_ref_asn1(child_issuer, parent_subject) != 0) { + ESP_LOGE(TAG, "Failed to reference ASN.1 data"); + mbedtls_x509_crt_free(new_cert); + free(new_cert); + return MBEDTLS_ERR_X509_ALLOC_FAILED; + } + + child_issuer = child_issuer->next; while (child_issuer != NULL) { - if (esp_crt_copy_asn1(child_issuer, parent_subject) != 0) { - ESP_LOGE(TAG, "Failed to copy ASN.1 data"); + parent_subject->next = calloc(1, sizeof(mbedtls_asn1_named_data)); + if (parent_subject->next == NULL) { + ESP_LOGE(TAG, "Failed to allocate memory for subject node"); + mbedtls_x509_crt_free(new_cert); + free(new_cert); + return MBEDTLS_ERR_X509_ALLOC_FAILED; + } + parent_subject = parent_subject->next; + if (esp_crt_ref_asn1(child_issuer, parent_subject) != 0) { + ESP_LOGE(TAG, "Failed to reference ASN.1 data"); mbedtls_x509_crt_free(new_cert); free(new_cert); return MBEDTLS_ERR_X509_ALLOC_FAILED; } child_issuer = child_issuer->next; - if (child_issuer == NULL) { - break; - } - - if (parent_subject->next == NULL) { - parent_subject->next = calloc(1, sizeof(mbedtls_asn1_named_data)); - if (parent_subject->next == NULL) { - ESP_LOGE(TAG, "Failed to allocate memory for next issuer"); - mbedtls_x509_crt_free(new_cert); - free(new_cert); - return MBEDTLS_ERR_X509_ALLOC_FAILED; - } - parent_subject = parent_subject->next; - } } // Set the parsed certificate as the candidate CA From 8554f060cc719f19ef818c4e7a1e663825e9db50 Mon Sep 17 00:00:00 2001 From: Ashish Sharma Date: Mon, 27 Apr 2026 17:26:42 +0800 Subject: [PATCH 2/3] fix(http_request): shifts to HTTP/1.1 in example pytest --- examples/protocols/http_request/pytest_http_request.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/protocols/http_request/pytest_http_request.py b/examples/protocols/http_request/pytest_http_request.py index b2a871e9830..7d6c7aa0c1a 100644 --- a/examples/protocols/http_request/pytest_http_request.py +++ b/examples/protocols/http_request/pytest_http_request.py @@ -22,7 +22,7 @@ def test_examples_protocol_http_request(dut: Dut) -> None: # check and log bin size binary_file = os.path.join(dut.app.binary_path, 'http_request.bin') bin_size = os.path.getsize(binary_file) - logging.info('http_request_bin_size : {}KB'.format(bin_size // 1024)) + logging.info(f'http_request_bin_size : {bin_size // 1024}KB') # start test dut.expect(r'DNS lookup succeeded.', timeout=30) # check if connected or not @@ -30,7 +30,7 @@ def test_examples_protocol_http_request(dut: Dut) -> None: dut.expect(' ... socket send success') dut.expect(' ... set socket receiving timeout success') # check server response - dut.expect(r'HTTP/1.0 200 OK') + dut.expect(r'HTTP/1.1 200 OK') # read from the socket completed dut.expect('... done reading from socket. Last read return=0 errno=128') dut.expect(r'(\d)...') From a1f1d9072900455702f8c3197ac7b98b7e19f180 Mon Sep 17 00:00:00 2001 From: Ashish Sharma Date: Tue, 28 Apr 2026 13:42:47 +0800 Subject: [PATCH 3/3] fix(esp_crt_bundle): fixes verification with cross signed cert --- .../mbedtls/esp_crt_bundle/esp_crt_bundle.c | 38 +++- .../mbedtls_ut/main/crts/prvtkey.pem | 55 +++--- .../mbedtls_ut/main/crts/server_cert_bundle | Bin 402 -> 402 bytes .../main/crts/server_cert_chain.pem | 60 ++++-- .../mbedtls_ut/main/crts/server_root.pem | 32 +-- .../mbedtls_ut/main/test_esp_crt_bundle.c | 183 +++++++++++++++++- .../test_apps/mbedtls_ut/pytest_mbedtls_ut.py | 19 ++ .../mbedtls_ut/sdkconfig.ci.cross_signed | 2 + 8 files changed, 325 insertions(+), 64 deletions(-) create mode 100644 components/mbedtls/test_apps/mbedtls_ut/sdkconfig.ci.cross_signed diff --git a/components/mbedtls/esp_crt_bundle/esp_crt_bundle.c b/components/mbedtls/esp_crt_bundle/esp_crt_bundle.c index 48339ed214b..0b271b73dc0 100644 --- a/components/mbedtls/esp_crt_bundle/esp_crt_bundle.c +++ b/components/mbedtls/esp_crt_bundle/esp_crt_bundle.c @@ -316,8 +316,33 @@ int esp_crt_verify_callback(void *buf, mbedtls_x509_crt* const crt, const int de { const mbedtls_x509_crt* const child = crt; - /* It's OK for a trusted cert to have a weak signature hash alg. - as we already trust this certificate */ +#if defined(CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_CROSS_SIGNED_VERIFY) + /* When cross-signed verification is enabled, the CA callback provides a + * synthetic bundle root containing only the subject name and public key. + * It has no meaningful validity window, so mbedtls may set EXPIRED/FUTURE + * on this generated cert. Clear those flags only for this synthetic bundle + * root so that cross-signed verification can continue. + * + * Real certificates must keep their time-based verification result and + * should not proceed to additional bundle signature checks once they are + * marked expired or not-yet-valid. */ + const uint32_t time_flags = *flags & + (MBEDTLS_X509_BADCERT_EXPIRED | MBEDTLS_X509_BADCERT_FUTURE); + if (time_flags && s_crt_bundle != NULL && child->raw.p == NULL && + child->valid_from.year == 0 && child->valid_to.year == 0) { + cert_t cert = esp_crt_find_cert(child->subject_raw.p, + child->subject_raw.len); + if (cert != NULL) { + *flags &= ~(MBEDTLS_X509_BADCERT_EXPIRED | + MBEDTLS_X509_BADCERT_FUTURE); + } + } +#endif /* CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_CROSS_SIGNED_VERIFY */ + + /* It's OK for a trusted bundle cert to have a weak signature hash alg, + * as we already trust this certificate. Do not ignore EXPIRED/FUTURE here: + * real certificates must fail on validity checks, and only the synthetic + * cross-signed bundle root has those flags cleared above. */ uint32_t flags_filtered = *flags & ~(MBEDTLS_X509_BADCERT_BAD_MD); if (flags_filtered != MBEDTLS_X509_BADCERT_NOT_TRUSTED) { @@ -339,6 +364,10 @@ int esp_crt_verify_callback(void *buf, mbedtls_x509_crt* const crt, const int de if (likely(ret == 0)) { ESP_LOGI(TAG, "Certificate validated"); + /* Bundle trust and signature verification succeeded. Real + * certificates with EXPIRED/FUTURE return earlier, and the + * synthetic cross-signed bundle root has those flags cleared + * above, so clear the remaining verification flags here. */ *flags = 0; return 0; } else { @@ -565,6 +594,11 @@ void esp_crt_bundle_detach(mbedtls_ssl_config *conf) s_crt_bundle = NULL; if (conf) { mbedtls_ssl_conf_verify(conf, NULL, NULL); +#if defined(CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_CROSS_SIGNED_VERIFY) + mbedtls_ssl_conf_ca_cb(conf, NULL, NULL); +#else + mbedtls_ssl_conf_ca_chain(conf, NULL, NULL); +#endif /* CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_CROSS_SIGNED_VERIFY */ } } diff --git a/components/mbedtls/test_apps/mbedtls_ut/main/crts/prvtkey.pem b/components/mbedtls/test_apps/mbedtls_ut/main/crts/prvtkey.pem index bb0a510a7cb..06aebb289be 100644 --- a/components/mbedtls/test_apps/mbedtls_ut/main/crts/prvtkey.pem +++ b/components/mbedtls/test_apps/mbedtls_ut/main/crts/prvtkey.pem @@ -1,27 +1,28 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEpQIBAAKCAQEAySb2QFrQZjQ1EhfN7I+raKrWSWWeYGppqPk3E1sV2y6LSE3M -7cVZyXxcpnP4mcKos3D9k8sbkt05oKcHR2THpWdz5mJn8A7TfJYWnYRHcRuR85th -XC8Pjf3f+JfjXgr/2a2JqHb4fttxoDRwWP1+kbTHa4iERqTFOIhYB9wD8uzbBHJq -IlIGbBHO9J+JxZKVkgDWZfc7zik3YzvkuWju/PmF73BagGxxRDzodzfxhHq1f96J -w10YS9pkhVFLxzOz3O+buwL8plCQplVnpj6J+2MaY4JlUvCcosDkT0EORAtrYGwZ -KJBKn4dRftk8Wbtns7zoJ2SNUL5TZ2oyAb2dwQIDAQABAoIBAQCRmE3tTs5A69Du -A6TdcTAUVnM8NP1ptBw+XgRrUiaDuzC9aPLHt2zB1e4J3S83vBn3p/UjIIQYzV+E -1OED4AJRyoutWdT5gQG6z7gW00QSrm358aGK49VSZUvT17yOuU9u85kMAvDigVvB -JbOb9f/C3yLoxqtXprPJs4ZkSe/hyB0JtRzauDYZnK4JmtgDfGds1cykohbUaeCW -ExUSbi/EPuroowmjEPFmN4tH/C3GtQonwGfjP76GXm8u5Fg8VXYUmb5pnxYcFdvv -shoasbK5lksgK50VP2vA9Y3mIrThRvkWgcv0TZaQWAF/JtdSXIID5WzfsgLbtDQF -hZLk1dDxAoGBAPfAjPAqapNVl4GqSkUWofUHMzZG85fHoPN3INSA0aMr4X9wHFfQ -pQ0ACxuimQj66Vk4rWww+HrsjPfiNMZzoi1exS1tjbQVyTBffrHj8sSdWt8Gw6MB -Pp5ubnCy9pl4lWNHlJZJp2SwAd10LzrizzAQALeEtRmg8meYGZElVUy7AoGBAM/Z -REXLJgaad5V3A2xehrSnknKUwab4LFIrgirZ6h0RXYo+wEHGJpDvM5Vw3sZT+UaJ -Jdlb3cXbqOxWrKlqjKe/S2vNScP7V2Na8l/ySO93PYE0V1Q+EeuNGi41xWC4Dh7o -D7BX2nDm9YBZzNVxM/30/dTzFM+CKrCARsLIXvWzAoGBAOQ4GRv61qXVyHSHO1cd -HB+sfD5ZaXa9S8Q6TqGx8GrQty4/RbyW1BN/oLvaMgKVr3KixQ3OpnYFhW2qkFbm -mdQVYqkQK+Jh1yyaKwkPI8h98wFTJ8/2C4rByzZBhOumqmYDwBoYyvvzLiSjLAag -e56YfzCOLIzpN6K594M+0q6VAoGBALWR5D1gKRjNqbetHxV1QhHg7WMhJkaZOAaU -MYMDmKvJ9sAE72jGE/y6qYJb9pCk3PdMaf8GbKciq9/CG9Vn2fXUe6txy4XkNEP8 -OA2vFx3yOY18Tumty3PNcNh7arCCOPuw17vCE3ZbnI2CZRj0amnosjFsJHreCDLl -7GrOJX5XAoGASZXbGykpYJTTr5PGPL/eX0koU1RZ9f6fvVdkfeWNGZfJ4oGkxDcO -fJnzq9wC9YREy6f3eoMrix95RPv4Qo1Wwi2PmtyMFvUdsYckFEhxSN3p4Iqn/nQg -6I7VB0yNqw8ZdP1vBkRcg3kk+QO2tci+OTdpDSKmO5nGjuqpsdBM5/o= ------END RSA PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCTJ5b+P/Qyla3v +ri/JphhHto/S39vE9GlnR3iP+7pKfmev89PmkQGvu1csGL4wHOhxtWh1KUqbgVGA +XXO0hVKGherY7XaugVg5td2kxrDeVVtx/mHf0cE8SqSdeTRNrIlBp43ogKb4QZdp +i019khCt6blB6Ky0vuFiud8ipDdZ+Iw1vokhpl2odjJ6wyMF6szDaWs3N/cVLAD5 +D8O8WgjWRvoQqjU/6UAaCXMAS7rQwolN2k4vjWuXAaq3TU6Kx8XQGS+3dqtWD6Vh +PetnnqPGrVZb8qC+ugN+Q9sNJlBlhaLDw3q6icS3wZT/x9B1sszZUzVl8L7dkvxd +A6SOuo8HAgMBAAECggEAM+P0BLf8npHVgf1EcLEXQsKHuO6jirI9MOQtCWjU6kvk +uFEc2eMsWxGOzcfz2Pd4qBA2Q4pJ+sgBO2i40mFpFnLGu7QQ87w8pi0ReXdvCxfC +ZVI0pT5MC7yui7Ef1nwO8o3pJqGnP0ex4s/yhWMqaolOgIVR0XK1+6BbpNWPhzPr +FqgvgLEnJWS0u2T2hOKFE37WFnM0pLuQl0oGwKOSpOaY9dFSUDMoPrmMCWEdAcuZ +qIz5GgmAwkHaA1ztnIec+Svm2sXSXTy0nrnyD/mcDOS0OCeCmxM5jZRJ1AkZIqm9 +PhHPVPEhHy+uR+jzvVE5lxUYPZman0t0dIaDuKqn0QKBgQDIwxpjJFjxkMVewZtD +wvFFVfkY7CVobZiTB1Tr7cL7/Bs6ge6ltnwG1sceuiTg23BzOgyt2cQpl+F7dDoU +QCXDWoO+Od3Qx7Sr0IQ3rwyycrez3DfcahmYieBiGRwIZx17ZcfpV8rsK8zg6enZ +4C/eV41RDG1dKJyDbXaYTzzY3QKBgQC7pJYfYKjyKwLuh5ORbt/OvnXSC99SrpMV +HaJRKgq6m6LPcCIz9CcZNLSj3coVojBOWWKC254GVghddrUxA77ntasQ9EYBAVmD +pBrMW62dJ+g9qZVLZw+3lF71nYmBAEIXulW5Do09hnmHLpQJQfRgfZQBsn0yryhH +h8+ZNOWXMwKBgCVxdfdtrQUIyjbdBxdBQXx8B2RljqiGYEFzyZvjEU2r/GwSFa9k +dIdWAXcyonQpmTR0bC70gYh8YjqDN93VBPYFaLLO2hb5WDH/RtmX1Vdm2+o4tVZv +l2yCso25Pyg2CyKbnghgLmGT3bdJCStwi5z9WUb3eWI5k89TWB4aETAlAoGBALsJ +7hJM2Vq7AOse/TtkV3bPZsX+y5axKS9NILTiwVsNNBat4YoD/s0jOkR1GbDCwH/4 +nTdvDm+mZiQz5Zx77VkuPtxhgT4TSFTtyUCWydHzK437cjN9Aa+uF5GgfKW3yim4 +tSYHmUYQuybCiRFJSvy7cELY1e8lpXLXr1k53vj9AoGBALNK7W6+f+y98/2e45Jd +SSEe1GF984hHAi4zRn64Zhg9gxKBM9CEyrsbL2BlP3UY+2xdnJvzKnmPtelhIvgH +FqG2XC6236PGzD9ETGyayNu/d612C82h/BeePzce/3o4WaMldbgMRdjniKl+nFJR +8EDuH1YACHhGCCE5BT24OtFs +-----END PRIVATE KEY----- diff --git a/components/mbedtls/test_apps/mbedtls_ut/main/crts/server_cert_bundle b/components/mbedtls/test_apps/mbedtls_ut/main/crts/server_cert_bundle index 5a0a2f139f52aa4576798324076a533e2a8457a2..16b7c3a6c699d2a6d664187dccad42c65839a6a1 100644 GIT binary patch delta 270 zcmV+p0rCEl1Cj%fjeq<&FFjC>OD%Z}>61XpxdO!Y6&nrnM#80(pm`B!K6}g5Z2J2U zLFe>TWA?T9T_rj-)vLu5njI}cKa#Ir8=<3gPdO|zwAj!H+GzviubyBp0oB$q*bW&< zJ#yxm!i6DypKOx^^Ig094FF{Y+zs&ImvSGl7Ef(pcnthtfm6`(T-58 z2U*P)RR0$>&UZw(z6vcem3j}#f1B$O_V5VV;t_uQqlRY{&}S0wE=uF=Q}Lvj>9)UWhM2G=+22QDpCg}wo}h|g5E{k&>Vg^r UE3EC+&y$(Deg_`e0s{d60q;bHF8}}l delta 270 zcmV+p0rCEl1Cj%fjen=o3(V;3&$n4@08}D?!Ud~$F#9j5-LxjjHyb$%jgQBTFpLKW zs@pqsu^Srsi~>H?G6KdAwhMn(G+5y=e2%G-kFux?qn44lrqJyd3y;AMFcA^ds3U z-AZL}yo7rn6G51-0Ikt4gcgs8_%xNLzMF&Mi Uo4 +#include +#include #include "esp_err.h" #include "esp_log.h" @@ -89,6 +91,38 @@ static volatile bool exit_flag; esp_err_t endpoint_teardown(mbedtls_endpoint_t *endpoint); +#if defined(CONFIG_MBEDTLS_HAVE_TIME_DATE) +/* Set system time to compile time so that MBEDTLS_HAVE_TIME_DATE checks + * pass without network/NTP. __DATE__ gives "Mon DD YYYY", __TIME__ gives "HH:MM:SS". */ +static void set_system_time_to_compile_time(void) +{ + const char *months[] = {"Jan","Feb","Mar","Apr","May","Jun", + "Jul","Aug","Sep","Oct","Nov","Dec"}; + char mon_str[4]; + int day, year, hour, min, sec; + + sscanf(__DATE__, "%3s %d %d", mon_str, &day, &year); + sscanf(__TIME__, "%d:%d:%d", &hour, &min, &sec); + + int mon = 0; + for (int i = 0; i < 12; i++) { + if (strcmp(mon_str, months[i]) == 0) { + mon = i; + break; + } + } + + struct tm t = { + .tm_sec = sec, .tm_min = min, .tm_hour = hour, + .tm_mday = day, .tm_mon = mon, .tm_year = year - 1900, + }; + + struct timeval tv = { .tv_sec = mktime(&t), .tv_usec = 0 }; + settimeofday(&tv, NULL); + ESP_LOGI(TAG, "System time set to compile time: %s %s", __DATE__, __TIME__); +} +#endif // CONFIG_MBEDTLS_HAVE_TIME_DATE + esp_err_t server_setup(mbedtls_endpoint_t *server) { int ret; @@ -302,7 +336,7 @@ void client_task(void *pvParameters) size_t available_before_handshake = uxTaskGetStackHighWaterMark(NULL); ESP_LOGI(TAG, "Available stack before handshake: %d", available_before_handshake); - ESP_LOGI(TAG, "Performing the SSL/TLS handshake with bundle that is missing the server root certificate"); + ESP_LOGI(TAG, "Performing the SSL/TLS handshake with bundle that contains the server root certificate"); while ( ( ret = mbedtls_ssl_handshake( &client->ssl ) ) != 0 ) { if ( ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE ) { printf( "mbedtls_ssl_handshake failed with -0x%x\n", -ret ); @@ -343,6 +377,10 @@ TEST_CASE("custom certificate bundle", "[mbedtls]") { test_case_uses_tcpip(); +#if defined(CONFIG_MBEDTLS_HAVE_TIME_DATE) + set_system_time_to_compile_time(); +#endif + SemaphoreHandle_t signal_sem = xSemaphoreCreateBinary(); TEST_ASSERT_NOT_NULL(signal_sem); @@ -567,3 +605,146 @@ TEST_CASE("custom certificate bundle init API - bound checking - Incorrect certi esp_ret = esp_crt_bundle_set(test_bundle, sizeof(test_bundle)); TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, esp_ret); } + +#if defined(CONFIG_MBEDTLS_HAVE_TIME_DATE) +TEST_CASE("certificate bundle - expired cert rejected with time-date check", "[mbedtls]") +{ + /* With HAVE_TIME_DATE enabled, a genuinely expired certificate must still + * be rejected even though its issuer is in the bundle and signature is valid. + * correct_sig_crt_esp32_com.pem expired Feb 2025 — with system time set to + * compile time (2026+), the EXPIRED flag must persist after bundle verification. */ + set_system_time_to_compile_time(); + + mbedtls_x509_crt crt; + uint32_t flags = 0; + + esp_crt_bundle_attach(NULL); + + mbedtls_x509_crt_init(&crt); + mbedtls_x509_crt_parse(&crt, correct_sig_crt_pem_start, + correct_sig_crt_pem_end - correct_sig_crt_pem_start); + + int ret = mbedtls_x509_crt_verify(&crt, NULL, NULL, NULL, &flags, + esp_crt_verify_callback, NULL); + + /* Verification must fail — the cert is genuinely expired */ + TEST_ASSERT_NOT_EQUAL(0, ret); + /* The EXPIRED flag specifically must be set */ + TEST_ASSERT_BITS_HIGH(MBEDTLS_X509_BADCERT_EXPIRED, flags); + + mbedtls_x509_crt_free(&crt); + esp_crt_bundle_detach(NULL); +} +#endif /* CONFIG_MBEDTLS_HAVE_TIME_DATE */ + +#if defined(CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_CROSS_SIGNED_VERIFY) && defined(CONFIG_MBEDTLS_HAVE_TIME_DATE) +/* Client task for cross-signed verification with time-date checking. + * Exercises the esp_crt_ca_cb_callback path where synthetic root certs + * from the bundle have no validity dates. */ +void client_task_cross_signed(void *pvParameters) +{ + SemaphoreHandle_t *client_signal_sem = (SemaphoreHandle_t *) pvParameters; + int ret = ESP_FAIL; + + mbedtls_endpoint_t *client = calloc(1, sizeof(mbedtls_endpoint_t)); + if (client == NULL) { + ESP_LOGE(TAG, "Failed to allocate memory for client"); + vTaskSuspend(NULL); + } + esp_crt_validate_res_t res = ESP_CRT_VALIDATE_UNKNOWN; + + if (client_setup(client) != ESP_OK) { + ESP_LOGE(TAG, "SSL client setup failed"); + goto exit; + } + + /* Attach bundle — this registers both esp_crt_verify_callback and + * esp_crt_ca_cb_callback when CROSS_SIGNED_VERIFY is enabled */ + ret = esp_crt_bundle_attach(&client->conf); + TEST_ASSERT_EQUAL(ESP_OK, ret); + + ret = esp_crt_bundle_set(server_cert_bundle_start, + server_cert_bundle_end - server_cert_bundle_start); + TEST_ASSERT_EQUAL(ESP_OK, ret); + + ESP_LOGI(TAG, "Connecting to %s:%s...", SERVER_ADDRESS, SERVER_PORT); + if ((ret = mbedtls_net_connect(&client->client_fd, SERVER_ADDRESS, + SERVER_PORT, MBEDTLS_NET_PROTO_TCP)) != 0) { + ESP_LOGE(TAG, "mbedtls_net_connect returned -%x", -ret); + goto exit; + } + + mbedtls_ssl_set_bio(&client->ssl, &client->client_fd, + mbedtls_net_send, mbedtls_net_recv, NULL); + + ESP_LOGI(TAG, "Performing SSL/TLS handshake (cross-signed verify + time-date)"); + while ((ret = mbedtls_ssl_handshake(&client->ssl)) != 0) { + if (ret != MBEDTLS_ERR_SSL_WANT_READ && + ret != MBEDTLS_ERR_SSL_WANT_WRITE) { + ESP_LOGE(TAG, "mbedtls_ssl_handshake failed with -0x%x", -ret); + break; + } + } + + ESP_LOGI(TAG, "Verifying peer X.509 certificate..."); + ret = mbedtls_ssl_get_verify_result(&client->ssl); + res = (ret == 0) ? ESP_CRT_VALIDATE_OK : ESP_CRT_VALIDATE_FAIL; + + if (res == ESP_CRT_VALIDATE_OK) { + ESP_LOGI(TAG, "Certificate verification passed!"); + } else { + ESP_LOGE(TAG, "Certificate verification failed! flags=0x%x", ret); + } + TEST_ASSERT_EQUAL(ESP_CRT_VALIDATE_OK, res); + +exit: + mbedtls_ssl_close_notify(&client->ssl); + mbedtls_ssl_session_reset(&client->ssl); + esp_crt_bundle_detach(&client->conf); + endpoint_teardown(client); + xSemaphoreGive(*client_signal_sem); + free(client); + vTaskSuspend(NULL); +} + +TEST_CASE("cross-signed certificate bundle with time-date check", "[mbedtls]") +{ + test_case_uses_tcpip(); + + /* Set system time so that certificate validity checks pass */ + set_system_time_to_compile_time(); + + SemaphoreHandle_t signal_sem = xSemaphoreCreateBinary(); + TEST_ASSERT_NOT_NULL(signal_sem); + + exit_flag = false; + TaskHandle_t server_task_handle; + xTaskCreate(server_task, "server task", 8192, &signal_sem, 10, + &server_task_handle); + + if (!xSemaphoreTake(signal_sem, SEM_TIMEOUT / portTICK_PERIOD_MS)) { + TEST_FAIL_MESSAGE("signal_sem not released, server start failed"); + } + + SemaphoreHandle_t client_signal_sem = xSemaphoreCreateBinary(); + TEST_ASSERT_NOT_NULL(client_signal_sem); + + TaskHandle_t client_task_handle; + xTaskCreate(client_task_cross_signed, "client task", 8192, + &client_signal_sem, 10, &client_task_handle); + + if (!xSemaphoreTake(client_signal_sem, SEM_TIMEOUT / portTICK_PERIOD_MS)) { + TEST_FAIL_MESSAGE("client_signal_sem not released, client exit failed"); + } + unity_utils_task_delete(client_task_handle); + + exit_flag = true; + + if (!xSemaphoreTake(signal_sem, SEM_TIMEOUT / portTICK_PERIOD_MS)) { + TEST_FAIL_MESSAGE("signal_sem not released, server exit failed"); + } + unity_utils_task_delete(server_task_handle); + vSemaphoreDelete(client_signal_sem); + vSemaphoreDelete(signal_sem); +} +#endif /* CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_CROSS_SIGNED_VERIFY && CONFIG_MBEDTLS_HAVE_TIME_DATE */ diff --git a/components/mbedtls/test_apps/mbedtls_ut/pytest_mbedtls_ut.py b/components/mbedtls/test_apps/mbedtls_ut/pytest_mbedtls_ut.py index 78392bb6014..3e5820959a1 100644 --- a/components/mbedtls/test_apps/mbedtls_ut/pytest_mbedtls_ut.py +++ b/components/mbedtls/test_apps/mbedtls_ut/pytest_mbedtls_ut.py @@ -155,3 +155,22 @@ def test_mbedtls_ds_rsa(dut: Dut) -> None: @idf_parametrize('target', ['esp32s3'], indirect=['target']) def test_mbedtls_aria(dut: Dut) -> None: dut.run_all_single_board_cases(group='aria') + + +@pytest.mark.generic +@pytest.mark.parametrize( + 'config', + [ + 'cross_signed', + ], + indirect=True, +) +@idf_parametrize('target', ['supported_targets'], indirect=['target']) +def test_mbedtls_cross_signed(dut: Dut) -> None: + dut.run_all_single_board_cases( + name=[ + 'cross-signed certificate bundle with time-date check', + 'custom certificate bundle', + 'certificate bundle - expired cert rejected with time-date check', + ] + ) diff --git a/components/mbedtls/test_apps/mbedtls_ut/sdkconfig.ci.cross_signed b/components/mbedtls/test_apps/mbedtls_ut/sdkconfig.ci.cross_signed new file mode 100644 index 00000000000..35866d2f491 --- /dev/null +++ b/components/mbedtls/test_apps/mbedtls_ut/sdkconfig.ci.cross_signed @@ -0,0 +1,2 @@ +CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_CROSS_SIGNED_VERIFY=y +CONFIG_MBEDTLS_HAVE_TIME_DATE=y