From 3c4d1960ec71106650c863a86b8b2c87f884da10 Mon Sep 17 00:00:00 2001 From: Daniel Kubec Date: Thu, 3 Sep 2026 14:28:23 +0200 Subject: [PATCH] TLS: Reject undersized TLS 1.2 AEAD records before AEAD processing In tls1_cipher() the receive path invoked EVP_CTRL_AEAD_TLS1_AAD before checking that the record was long enough to contain the mandatory explicit IV and authentication tag. A record shorter than that overhead made the provider reject the impossible length, and the error path raised SSL_AD_INTERNAL_ERROR. Validate the record length against the explicit IV and tag length before AEAD processing and return failure without raising an alert, leaving the caller to react correctly: TLS reports bad_record_mac and DTLS silently discards the record. Co-Authored-By: Mounir IDRASSI Fixes CVE-2026-75806 Reviewed-by: Milan Broz Reviewed-by: Tomas Mraz Merge-date: Tue Sep 29 11:08:50 2026 --- ssl/record/methods/tls1_meth.c | 8 ++ test/recordlentest.c | 218 ++++++++++++++++++++++++++++++++- 2 files changed, 225 insertions(+), 1 deletion(-) diff --git a/ssl/record/methods/tls1_meth.c b/ssl/record/methods/tls1_meth.c index ed4a436dff..6bf7bb46e3 100644 --- a/ssl/record/methods/tls1_meth.c +++ b/ssl/record/methods/tls1_meth.c @@ -261,6 +261,14 @@ static int tls1_cipher(OSSL_RECORD_LAYER *rl, TLS_RL_RECORD *recs, != 0) { unsigned char *seq; + /* + * Publicly invalid: the record is shorter than the mandatory + * AEAD overhead. Leave alert handling to the caller so TLS + * reports bad_record_mac and DTLS silently discards the record. + */ + if (!sending && reclen[ctr] < rl->eivlen + rl->taglen) + return 0; + seq = rl->sequence; if (rl->isdtls) { diff --git a/test/recordlentest.c b/test/recordlentest.c index c38ca3903f..01003ffd7f 100644 --- a/test/recordlentest.c +++ b/test/recordlentest.c @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 The OpenSSL Project Authors. All Rights Reserved. + * Copyright 2017-2026 The OpenSSL Project Authors. All Rights Reserved. * * Licensed under the Apache License 2.0 (the "License"). You may not use * this file except in compliance with the License. You can obtain a copy @@ -9,6 +9,8 @@ #include +#include + #include "helpers/ssltestlib.h" #include "testutil.h" @@ -60,6 +62,31 @@ static int write_record(BIO *b, size_t len, uint8_t rectype, int recversion) return 1; } +#if !defined(OPENSSL_NO_DTLS1_2) +static int write_dtls_record(BIO *b, size_t len, uint8_t rectype, + int recversion, uint16_t epoch, uint64_t seq) +{ + unsigned char record[DTLS1_RT_HEADER_LENGTH + 256] = { 0 }; + size_t written, i; + + if (len > sizeof(record) - DTLS1_RT_HEADER_LENGTH) + return 0; + + record[0] = rectype; + record[1] = (recversion >> 8) & 0xff; + record[2] = recversion & 0xff; + record[3] = (epoch >> 8) & 0xff; + record[4] = epoch & 0xff; + for (i = 0; i < 6; i++) + record[10 - i] = (seq >> (8 * i)) & 0xff; + record[11] = (len >> 8) & 0xff; + record[12] = len & 0xff; + + return BIO_write_ex(b, record, DTLS1_RT_HEADER_LENGTH + len, &written) + && written == DTLS1_RT_HEADER_LENGTH + len; +} +#endif + static int fail_due_to_record_overflow(int enc) { long err = ERR_peek_error(); @@ -181,6 +208,188 @@ end: return testresult; } +static int write_and_read_app_data(SSL *serverssl, SSL *clientssl) +{ + static const unsigned char msg = 0xa5; + unsigned char buf = 0; + size_t readbytes = 0, written = 0; + + return TEST_true(SSL_write_ex(clientssl, &msg, sizeof(msg), &written)) + && TEST_size_t_eq(written, sizeof(msg)) + && TEST_true(SSL_read_ex(serverssl, &buf, sizeof(buf), + &readbytes)) + && TEST_size_t_eq(readbytes, sizeof(msg)) + && TEST_uchar_eq(buf, msg); +} + +typedef enum { + SHORT_AEAD_RECORD_AES, + SHORT_AEAD_RECORD_ARIA, + SHORT_AEAD_RECORD_CHACHA +} SHORT_AEAD_RECORD_CIPHER; + +typedef struct { + const char *cipher; + size_t record_len; + SHORT_AEAD_RECORD_CIPHER cipher_type; +} SHORT_AEAD_RECORD_TEST; + +#if !defined(OPENSSL_NO_TLS1_2) +static const SHORT_AEAD_RECORD_TEST short_aead_record_tests[] = { + { TLS1_TXT_RSA_WITH_AES_128_CCM, 0, SHORT_AEAD_RECORD_AES }, + { TLS1_TXT_RSA_WITH_AES_128_CCM, + EVP_CCM_TLS_EXPLICIT_IV_LEN + EVP_CCM_TLS_TAG_LEN - 1, + SHORT_AEAD_RECORD_AES }, + { TLS1_TXT_RSA_WITH_AES_128_CCM_8, + EVP_CCM_TLS_EXPLICIT_IV_LEN + EVP_CCM8_TLS_TAG_LEN - 1, + SHORT_AEAD_RECORD_AES }, + { TLS1_TXT_RSA_WITH_AES_128_GCM_SHA256, + EVP_GCM_TLS_EXPLICIT_IV_LEN + EVP_GCM_TLS_TAG_LEN - 1, + SHORT_AEAD_RECORD_AES }, + { TLS1_TXT_RSA_WITH_ARIA_128_GCM_SHA256, + EVP_GCM_TLS_EXPLICIT_IV_LEN + EVP_GCM_TLS_TAG_LEN - 1, + SHORT_AEAD_RECORD_ARIA }, + { TLS1_TXT_ECDHE_RSA_WITH_CHACHA20_POLY1305, + EVP_CHACHAPOLY_TLS_TAG_LEN - 1, SHORT_AEAD_RECORD_CHACHA }, +}; +#endif + +static void alert_cb(int write_p, int version, int content_type, + const void *buf, size_t len, SSL *ssl, void *arg) +{ + unsigned char *alert = arg; + const unsigned char *alert_data = buf; + + if (write_p && content_type == SSL3_RT_ALERT && len == 2) { + alert[0] = alert_data[0]; + alert[1] = alert_data[1]; + } +} + +#ifndef OPENSSL_NO_TLS1_2 +static int test_tls12_short_aead_record(int idx) +{ + const SHORT_AEAD_RECORD_TEST *test = &short_aead_record_tests[idx]; + SSL_CTX *cctx = NULL, *sctx = NULL; + SSL *clientssl = NULL, *serverssl = NULL; + int testresult = 0; + size_t readbytes; + unsigned char buf, alert[2] = { 0 }; + +#ifdef OPENSSL_NO_AES + if (test->cipher_type == SHORT_AEAD_RECORD_AES) + return TEST_skip("AES is disabled"); +#endif +#ifdef OPENSSL_NO_ARIA + if (test->cipher_type == SHORT_AEAD_RECORD_ARIA) + return TEST_skip("ARIA is disabled"); +#endif +#if defined(OPENSSL_NO_CHACHA) || defined(OPENSSL_NO_POLY1305) \ + || defined(OPENSSL_NO_EC) + if (test->cipher_type == SHORT_AEAD_RECORD_CHACHA) + return TEST_skip("ChaCha20-Poly1305 or EC is disabled"); +#endif + + if (!TEST_true(create_ssl_ctx_pair(NULL, TLS_server_method(), + TLS_client_method(), TLS1_2_VERSION, TLS1_2_VERSION, + &sctx, &cctx, cert, privkey))) + goto end; + + /* CCM-8 is not available at the default security level. */ + SSL_CTX_set_security_level(sctx, 0); + SSL_CTX_set_security_level(cctx, 0); + + if (!TEST_true(SSL_CTX_set_cipher_list(sctx, test->cipher)) + || !TEST_true(SSL_CTX_set_cipher_list(cctx, test->cipher)) + || !TEST_true(create_ssl_objects(sctx, cctx, &serverssl, &clientssl, + NULL, NULL))) + goto end; + + SSL_set_msg_callback(serverssl, alert_cb); + SSL_set_msg_callback_arg(serverssl, &alert); + + if (!TEST_true(create_ssl_connection(serverssl, clientssl, + SSL_ERROR_NONE)) + || !TEST_true(write_and_read_app_data(serverssl, clientssl)) + || !TEST_true(write_record(SSL_get_rbio(serverssl), test->record_len, + SSL3_RT_APPLICATION_DATA, TLS1_2_VERSION)) + || !TEST_false(SSL_read_ex(serverssl, &buf, sizeof(buf), &readbytes)) + || !TEST_uchar_eq(alert[0], SSL3_AL_FATAL) + || !TEST_uchar_eq(alert[1], SSL_AD_BAD_RECORD_MAC)) + goto end; + + testresult = 1; +end: + SSL_free(serverssl); + SSL_free(clientssl); + SSL_CTX_free(sctx); + SSL_CTX_free(cctx); + return testresult; +} +#endif + +#ifndef OPENSSL_NO_DTLS1_2 +static int test_dtls12_short_aead_record(void) +{ + SSL_CTX *cctx = NULL, *sctx = NULL; + SSL *clientssl = NULL, *serverssl = NULL; + int testresult = 0, ret; + size_t readbytes = 0; + unsigned char buf = 0, alert[2] = { 0 }; + +#ifdef OPENSSL_NO_AES + return TEST_skip("AES is disabled"); +#endif + + if (!TEST_true(create_ssl_ctx_pair(NULL, DTLS_server_method(), + DTLS_client_method(), DTLS1_2_VERSION, DTLS1_2_VERSION, + &sctx, &cctx, cert, privkey))) + goto end; + + SSL_CTX_set_security_level(sctx, 0); + SSL_CTX_set_security_level(cctx, 0); + + if (!TEST_true(SSL_CTX_set_cipher_list(sctx, + TLS1_TXT_RSA_WITH_AES_128_GCM_SHA256)) + || !TEST_true(SSL_CTX_set_cipher_list(cctx, + TLS1_TXT_RSA_WITH_AES_128_GCM_SHA256)) + || !TEST_true(create_ssl_objects(sctx, cctx, &serverssl, &clientssl, + NULL, NULL))) + goto end; + + SSL_set_msg_callback(serverssl, alert_cb); + SSL_set_msg_callback_arg(serverssl, &alert); + + /* + * The forged record uses the first application-data sequence number. A + * valid client write after this must still be accepted. + */ + if (!TEST_true(create_ssl_connection(serverssl, clientssl, + SSL_ERROR_NONE)) + || !TEST_true(write_dtls_record(SSL_get_rbio(serverssl), + EVP_GCM_TLS_EXPLICIT_IV_LEN + EVP_GCM_TLS_TAG_LEN - 1, + SSL3_RT_APPLICATION_DATA, DTLS1_2_VERSION, 1, 1))) + goto end; + + ERR_clear_error(); + ret = SSL_read_ex(serverssl, &buf, sizeof(buf), &readbytes); + if (!TEST_false(ret) + || !TEST_int_eq(SSL_get_error(serverssl, ret), SSL_ERROR_WANT_READ) + || !TEST_uchar_eq(alert[0], 0) + || !TEST_uchar_eq(alert[1], 0) + || !TEST_true(write_and_read_app_data(serverssl, clientssl))) + goto end; + + testresult = 1; +end: + SSL_free(serverssl); + SSL_free(clientssl); + SSL_CTX_free(sctx); + SSL_CTX_free(cctx); + return testresult; +} +#endif + OPT_TEST_DECLARE_USAGE("certfile privkeyfile\n") int setup_tests(void) @@ -195,6 +404,13 @@ int setup_tests(void) return 0; ADD_ALL_TESTS(test_record_overflow, TOTAL_RECORD_OVERFLOW_TESTS); +#ifndef OPENSSL_NO_TLS1_2 + ADD_ALL_TESTS(test_tls12_short_aead_record, + OSSL_NELEM(short_aead_record_tests)); +#endif +#ifndef OPENSSL_NO_DTLS1_2 + ADD_TEST(test_dtls12_short_aead_record); +#endif return 1; } -- 2.55.0