Commit bf416baf7d for openssl.org

commit bf416baf7d7c69b5ca64ebac206b6f129a0708f2
Author: Ryan Hooper <ryanh@openssl.foundation>
Date:   Mon Aug 31 16:31:46 2026 -0400

    DTLS 1.3 Enforce the RFC 9147 Section 8 sending epoch limit

    dtls1_increment_epoch() capped the read/write epoch at UINT16_MAX for
    DTLS 1.2, but skipped that check entirely for DTLS 1.3, relying only
    on the full 64-bit wrap as a backstop. RFC 9147 Section 8 requires a
    much tighter bound for senders: "sending implementations MUST NOT
    allow the epoch to exceed 2^48-1", motivated by AES-128's 128-bit key
    giving a non-negligible key-reuse probability at anything near 2^64
    rekeys. Receivers are held to the looser Section 6.1 ceiling instead,
    so this is intentionally a write-side-only, DTLS-1.3-only check.

    Add DTLS1_3_MAX_EPOCH (2^48-1) and enforce it in the write branch of
    dtls1_increment_epoch() before the increment, mirroring the existing
    UINT16_MAX guard's pre-increment style. With both the DTLS 1.2 and
    DTLS 1.3 write-side ceilings now always intercepting w_conn_epoch
    well below UINT64_MAX, the post-increment wrap-to-zero check on that
    path can no longer be reached, so it is removed.

    Add test_dtls13_increment_epoch_max() to test/dtls13_internal_test.c,
    which negotiates a real DTLS 1.3 connection (SSL_CONNECTION_IS_DTLS13()
    depends on the negotiated method and can't be forced directly), then
    writes w_conn_epoch to one below the limit and calls the real
    increment function at and past the boundary.

    Fixes: https://github.com/openssl/openssl/issues/32511
    Assisted-by: Claude:claude-sonnet-5
    Reviewed-by: Frederik Wedel-Heinen <fwh.openssl@gmail.com>
    Reviewed-by: Jakub Zelenka <jakub.zelenka@openssl.foundation>
    Merge-date: Mon Sep  7 14:28:46 2026
    Merged-from: https://github.com/openssl/openssl/pull/32617

diff --git a/ssl/record/rec_layer_d1.c b/ssl/record/rec_layer_d1.c
index 1366c54d15..38337c9381 100644
--- a/ssl/record/rec_layer_d1.c
+++ b/ssl/record/rec_layer_d1.c
@@ -840,14 +840,17 @@ int dtls1_increment_epoch(SSL_CONNECTION *s, int rw)
             /* We've wrapped around, so clear the buffer just in case */
             return 0;
     } else {
-        if (!SSL_CONNECTION_IS_DTLS13(s) && s->rlayer.d->w_conn_epoch == UINT16_MAX)
+        if (!SSL_CONNECTION_IS_DTLS13(s) && s->rlayer.d->w_conn_epoch == DTLS1_MAX_EPOCH)
             return 0;

-        s->rlayer.d->w_conn_epoch++;
-
-        if (s->rlayer.d->w_conn_epoch == 0)
-            /* We've wrapped around, so clear the buffer just in case */
+        /*
+         * RFC 9147 Section 8: sending implementations MUST NOT allow the
+         * epoch to exceed 2^48-1.
+         */
+        if (SSL_CONNECTION_IS_DTLS13(s) && s->rlayer.d->w_conn_epoch == DTLS1_3_MAX_EPOCH)
             return 0;
+
+        s->rlayer.d->w_conn_epoch++;
     }

     return 1;
diff --git a/ssl/record/record.h b/ssl/record/record.h
index 7265173c65..13b709c2bf 100644
--- a/ssl/record/record.h
+++ b/ssl/record/record.h
@@ -48,6 +48,14 @@ typedef struct tls_record_st {
 #endif
 } TLS_RECORD;

+/*
+ * RFC 9147 Section 8: sending implementations MUST NOT allow the epoch to
+ * exceed 2^48-1. This margin (rather than the 2^64-1 wire/representation
+ * ceiling of Section 6.1) is what actually bounds w_conn_epoch for DTLS 1.3.
+ */
+#define DTLS1_3_MAX_EPOCH ((uint64_t)0xFFFFFFFFFFFFULL)
+#define DTLS1_MAX_EPOCH UINT16_MAX
+
 typedef struct dtls_record_layer_st {
     /*
      * The current data and handshake epoch. This is initially
diff --git a/test/build.info b/test/build.info
index 0d4c0937a6..74469ae9e2 100644
--- a/test/build.info
+++ b/test/build.info
@@ -821,7 +821,7 @@ IF[{- !$disabled{tests} -}]
   INCLUDE[pemtest]=../include ../apps/include
   DEPEND[pemtest]=../libcrypto libtestutil.a

-  SOURCE[dtls13_internal_test]=dtls13_internal_test.c
+  SOURCE[dtls13_internal_test]=dtls13_internal_test.c helpers/ssltestlib.c
   INCLUDE[dtls13_internal_test]=.. ../include ../apps/include
   DEPEND[dtls13_internal_test]=../libssl.a ../libcrypto.a libtestutil.a

diff --git a/test/dtls13_internal_test.c b/test/dtls13_internal_test.c
index 8bda47fdd0..8c2a580d4f 100644
--- a/test/dtls13_internal_test.c
+++ b/test/dtls13_internal_test.c
@@ -8,9 +8,16 @@
  */

 #include "../ssl/record/methods/recmethod_local.h"
+#include "../ssl/ssl_local.h"
 #include "internal/nelem.h"
+#include "internal/ssl_unwrap.h"
+#include "helpers/ssltestlib.h"
 #include "testutil.h"
 #include <openssl/evp.h>
+#include <openssl/ssl.h>
+
+static char *cert = NULL;
+static char *privkey = NULL;

 static const char *cipher_names[] = {
     "aes-128-ecb",
@@ -158,9 +165,83 @@ static int test_seq_num_reconstruction(int idx)
         t->truncated);
 }

+#ifndef OPENSSL_NO_DTLS1_3
+/*
+ * Test that dtls1_increment_epoch() enforces the RFC 9147 Section 8 limit
+ * on the write (sending) epoch for DTLS 1.3: "sending implementations MUST
+ * NOT allow the epoch to exceed 2^48-1". This is stricter than the 2^64-1
+ * wrap-around ceiling in Section 6.1, and applies only to the write side
+ * and only for DTLS 1.3 -- DTLS 1.2 keeps its existing UINT16_MAX limit.
+ *
+ * There's no way to actually drive 2^48 real KeyUpdates in a test, so this
+ * drives one real DTLS 1.3 handshake to get a genuinely-typed connection
+ * (SSL_CONNECTION_IS_DTLS13() depends on the negotiated method, not
+ * anything that can be poked directly), then writes w_conn_epoch directly
+ * to one below the limit before calling the real increment function at and
+ * past the boundary.
+ */
+static int test_dtls13_increment_epoch_max(void)
+{
+    SSL_CTX *sctx = NULL, *cctx = NULL;
+    SSL *serverssl = NULL, *clientssl = NULL;
+    SSL_CONNECTION *sc = NULL;
+    int testresult = 0;
+
+    if (!TEST_true(create_ssl_ctx_pair(NULL, DTLS_server_method(),
+            DTLS_client_method(),
+            DTLS1_3_VERSION, DTLS1_3_VERSION,
+            &sctx, &cctx, cert, privkey)))
+        goto end;
+
+    if (!TEST_true(create_ssl_objects(sctx, cctx, &serverssl, &clientssl,
+            NULL, NULL)))
+        goto end;
+
+    if (!TEST_true(create_ssl_connection(serverssl, clientssl, SSL_ERROR_NONE)))
+        goto end;
+
+    if (!TEST_int_eq(SSL_version(serverssl), DTLS1_3_VERSION))
+        goto end;
+
+    if (!TEST_ptr(sc = SSL_CONNECTION_FROM_SSL(serverssl)))
+        goto end;
+
+    if (!TEST_true(SSL_CONNECTION_IS_DTLS13(sc)))
+        goto end;
+
+    /* One below the Section 8 limit: incrementing must still succeed. */
+    sc->rlayer.d->w_conn_epoch = DTLS1_3_MAX_EPOCH - 1;
+    if (!TEST_true(dtls1_increment_epoch(sc, SSL3_CC_WRITE)))
+        goto end;
+    if (!TEST_uint64_t_eq(sc->rlayer.d->w_conn_epoch, DTLS1_3_MAX_EPOCH))
+        goto end;
+
+    /* Already at the limit: incrementing further must be rejected. */
+    if (!TEST_false(dtls1_increment_epoch(sc, SSL3_CC_WRITE)))
+        goto end;
+    if (!TEST_uint64_t_eq(sc->rlayer.d->w_conn_epoch, DTLS1_3_MAX_EPOCH))
+        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 */
+
 int setup_tests(void)
 {
+    if (!TEST_ptr(cert = test_get_argument(0))
+        || !TEST_ptr(privkey = test_get_argument(1)))
+        return 0;
+
     ADD_ALL_TESTS(test_dtls_crypt_sequence_number, OSSL_NELEM(cipher_names));
     ADD_ALL_TESTS(test_seq_num_reconstruction, OSSL_NELEM(seq_num_tests));
+#ifndef OPENSSL_NO_DTLS1_3
+    ADD_TEST(test_dtls13_increment_epoch_max);
+#endif
     return 1;
 }
diff --git a/test/recipes/60-test_dtls13_internal.t b/test/recipes/60-test_dtls13_internal.t
index c070109d84..4d2f080ea8 100644
--- a/test/recipes/60-test_dtls13_internal.t
+++ b/test/recipes/60-test_dtls13_internal.t
@@ -7,6 +7,11 @@
 # https://www.openssl.org/source/license.html


-use OpenSSL::Test::Simple;
+use OpenSSL::Test qw/:DEFAULT srctop_file/;

-simple_test("test_dtls13_internal", "dtls13_internal_test");
+setup("test_dtls13_internal");
+
+plan tests => 1;
+
+ok(run(test(["dtls13_internal_test", srctop_file("apps", "server.pem"),
+             srctop_file("apps", "server.pem")])), "running dtls13_internal_test");