Commit a012e34902 for openssl.org

commit a012e3490242861fbf1b6b88edaa888f591d989f
Author: Ryan Hooper <ryanh@openssl.foundation>
Date:   Tue Sep 8 14:17:18 2026 -0400

    DTLS1.3: disallow TLS_AES_128_CCM_8_SHA256 under DTLS1.3

    RFC 9147 requires that TLS_AES_128_CCM_8_SHA256 not be used with DTLS
    without additional safeguards against forgery, due to its short
    authentication tag. We implement no such safeguards, so exclude this
    ciphersuite from DTLS negotiation by clearing its min_dtls/max_dtls
    bounds in the cipher table. It remains fully available for TLS.

    Update the existing tests that assumed this cipher was negotiable
    under DTLS1.3 (test_tls13_ciphersuite, early_data_skip_helper,
    test_early_data_psk_with_all_ciphers), and add a dedicated regression
    test confirming a DTLS1.3 connection offering only this ciphersuite
    fails to find a usable cipher. Document the restriction in the
    DTLS1.3 guide.

    Fixes: https://github.com/openssl/openssl/issues/32509
    Assisted-by: Claude:claude-sonnet-5
    Reviewed-by: Todd Short <todd.short@me.com>
    Reviewed-by: Frederik Wedel-Heinen <fwh.openssl@gmail.com>
    Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
    Merge-date: Mon Sep 14 14:04:07 2026
    Merged-from: https://github.com/openssl/openssl/pull/32749

diff --git a/doc/man7/ossl-guide-dtlsv13.pod b/doc/man7/ossl-guide-dtlsv13.pod
index 6af8582e97..fc054ff756 100644
--- a/doc/man7/ossl-guide-dtlsv13.pod
+++ b/doc/man7/ossl-guide-dtlsv13.pod
@@ -228,6 +228,14 @@ OpenSSL does not support Connection IDs in DTLSv1.3. If a client that supports C
 IDs connects to an OpenSSL server, the server will ignore the Connection ID and will not
 include a Connection ID in its responses.

+=head1 CIPHER SUITES
+
+RFC 9147 requires that TLS_AES_128_CCM_8_SHA256 must not be used with DTLS
+without additional safeguards against forgery, due to its short
+authentication tag. OpenSSL does not implement such safeguards, so this
+ciphersuite is not available when negotiating DTLS, even though it remains
+available for TLS.
+
 =head1 HOW TO UTILIZE DTLSV1.3 IN OPENSSL

 =head2 SSL_set_min_proto_version and SSL_set_max_proto_version
diff --git a/ssl/s3_lib.c b/ssl/s3_lib.c
index ee888c2cef..83b4b560bc 100644
--- a/ssl/s3_lib.c
+++ b/ssl/s3_lib.c
@@ -121,8 +121,13 @@ static SSL_CIPHER tls13_ciphers[] = {
         SSL_AEAD,
         TLS1_3_VERSION,
         TLS1_3_VERSION,
-        DTLS1_3_VERSION,
-        DTLS1_3_VERSION,
+        /*
+         * RFC 9147 (DTLS 1.3): "TLS_AES_128_CCM_8_SHA256 MUST NOT be used in
+         * DTLS without additional safeguards against forgery." We implement
+         * no such safeguards, so this cipher is not available under DTLS.
+         */
+        0,
+        0,
         SSL_NOT_DEFAULT | SSL_MEDIUM,
         SSL_HANDSHAKE_MAC_SHA256,
         64, /* CCM8 uses a short tag, so we have a low security strength */
diff --git a/test/dtlstest.c b/test/dtlstest.c
index ad7f8f2792..6ec94b8e52 100644
--- a/test/dtlstest.c
+++ b/test/dtlstest.c
@@ -1474,6 +1474,66 @@ end:

     return testresult;
 }
+
+/*
+ * RFC 9147 (DTLS 1.3): TLS_AES_128_CCM_8_SHA256 MUST NOT be used in DTLS
+ * without additional safeguards against forgery, due to its short
+ * authentication tag. OpenSSL does not implement such safeguards, so a
+ * DTLS1.3 connection offering only this ciphersuite must fail to find a
+ * usable cipher rather than falling back to negotiating it anyway.
+ */
+static int test_dtls13_ccm8_not_offered(void)
+{
+    SSL_CTX *sctx = NULL, *cctx = NULL;
+    SSL *serverssl = NULL, *clientssl = NULL;
+    int testresult = 0;
+    int ret;
+
+    if (!TEST_true(create_ssl_ctx_pair(NULL, DTLS_server_method(),
+            DTLS_client_method(),
+            DTLS1_3_VERSION, DTLS1_3_VERSION,
+            &sctx, &cctx, cert, privkey)))
+        return 0;
+
+    /* CCM8 ciphers are considered low security due to their short tag */
+    SSL_CTX_set_security_level(sctx, 0);
+    SSL_CTX_set_security_level(cctx, 0);
+
+    if (!TEST_true(SSL_CTX_set_ciphersuites(sctx, TLS1_3_RFC_AES_128_CCM_8_SHA256))
+        || !TEST_true(SSL_CTX_set_ciphersuites(cctx, TLS1_3_RFC_AES_128_CCM_8_SHA256)))
+        goto end;
+
+    if (!TEST_true(create_ssl_objects(sctx, cctx, &serverssl, &clientssl,
+            NULL, NULL)))
+        goto end;
+
+    /*
+     * The client must fail before it can even construct a ClientHello: it
+     * has no cipher left that is permitted under DTLS to offer.
+     */
+    if (!TEST_int_le(ret = SSL_connect(clientssl), 0)
+        || !TEST_int_eq(SSL_get_error(clientssl, ret), SSL_ERROR_SSL)
+        || !TEST_int_eq(ERR_GET_REASON(ERR_get_error()),
+            SSL_R_NO_CIPHERS_AVAILABLE))
+        goto end;
+    ERR_clear_error();
+
+    /* The server independently has nothing usable configured either */
+    if (!TEST_int_le(ret = SSL_accept(serverssl), 0)
+        || !TEST_int_eq(SSL_get_error(serverssl, ret), SSL_ERROR_SSL)
+        || !TEST_int_eq(ERR_GET_REASON(ERR_get_error()),
+            SSL_R_NO_CIPHERS_AVAILABLE))
+        goto end;
+
+    testresult = 1;
+end:
+    SSL_free(serverssl);
+    SSL_free(clientssl);
+    SSL_CTX_free(sctx);
+    SSL_CTX_free(cctx);
+
+    return testresult;
+}
 #endif /* OPENSSL_NO_DTLS1_3 */

 /* Confirm that we can create a connections using DTLSv1_listen() */
@@ -1561,6 +1621,7 @@ int setup_tests(void)
     ADD_ALL_TESTS(test_dtls13_forged_plaintext_alert, 8);
     ADD_ALL_TESTS(test_dtls13_forged_plaintext_alert_plant, 3);
     ADD_TEST(test_dtls13_epoch0_plaintext_alert);
+    ADD_TEST(test_dtls13_ccm8_not_offered);
 #endif

     return 1;
diff --git a/test/sslapitest.c b/test/sslapitest.c
index 1aa43c525c..7055f78b3f 100644
--- a/test/sslapitest.c
+++ b/test/sslapitest.c
@@ -5138,6 +5138,14 @@ static int early_data_skip_helper(int testdtls, int testtype, int cipher, int id
     if (is_fips && cipher >= 4)
         return 1;

+    /*
+     * RFC 9147 (DTLS 1.3): TLS_AES_128_CCM_8_SHA256 MUST NOT be used in
+     * DTLS without additional safeguards against forgery, which we do not
+     * implement, so this cipher is not offered under DTLS.
+     */
+    if (testdtls && cipher == 0)
+        return 1;
+
     if (ciphersuites[cipher] == NULL)
         return TEST_skip("Cipher not supported");

@@ -5842,6 +5850,13 @@ static int test_early_data_psk_with_all_ciphers(int idx)
      */
     if ((idx == 2 || idx == 5 || idx == 6) && is_fips == 1)
         return 1;
+    /*
+     * RFC 9147 (DTLS 1.3): TLS_AES_128_CCM_8_SHA256 MUST NOT be used in
+     * DTLS without additional safeguards against forgery, which we do not
+     * implement, so this cipher is not offered under DTLS.
+     */
+    if (idx == 4 && testdtls)
+        return 1;

     /* We always set this up with a final parameter of "2" for PSK */
     if (!TEST_true(setupearly_data_test(&cctx, &sctx, &clientssl,
@@ -7103,10 +7118,26 @@ static int test_tls13_ciphersuite(int idx)
              * TEST_strn_eq is used below because t13_cipher can contain
              * multiple ciphersuites
              */
-            if (max_ver == version1_3
-                && !TEST_strn_eq(t13_cipher, negotiated_scipher,
-                    strlen(negotiated_scipher)))
-                goto end;
+            if (max_ver == version1_3) {
+                const char *expected = t13_cipher;
+
+                /*
+                 * TLS_AES_128_CCM_8_SHA256 is not offered under DTLS (RFC
+                 * 9147 forbids it without additional forgery safeguards we
+                 * do not implement). When an entry lists it first for a
+                 * DTLS1.3 run, the next cipher in the list is what actually
+                 * gets negotiated instead.
+                 */
+                if (testdtls
+                    && strncmp(t13_cipher, TLS1_3_RFC_AES_128_CCM_8_SHA256,
+                           strlen(TLS1_3_RFC_AES_128_CCM_8_SHA256))
+                        == 0)
+                    expected = TLS1_3_RFC_AES_128_CCM_SHA256;
+
+                if (!TEST_strn_eq(expected, negotiated_scipher,
+                        strlen(negotiated_scipher)))
+                    goto end;
+            }

 #ifndef OPENSSL_NO_TLS1_2
             /* Below validation is not done when t12_cipher is NULL */