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");