From 40d526b056cdf0578ba475b25ba86fe794b63f02 Mon Sep 17 00:00:00 2001 From: Josh Holtrop Date: Wed, 27 May 2026 14:01:07 -0400 Subject: [PATCH] Support importing/exporting DTLS sessions with encrypt-then-mac options --- src/internal.c | 28 +++++++++++-- tests/api/test_dtls.c | 98 +++++++++++++++++++++++++++++++++++++++++++ tests/api/test_dtls.h | 4 +- wolfssl/internal.h | 23 +++++----- 4 files changed, 138 insertions(+), 15 deletions(-) diff --git a/src/internal.c b/src/internal.c index 75c007c657..6d12e325c5 100644 --- a/src/internal.c +++ b/src/internal.c @@ -1423,7 +1423,9 @@ static int ExportOptions(WOLFSSL* ssl, byte* exp, word32 len, byte ver, exp[idx++] = options->acceptState; exp[idx++] = options->asyncState; - if (type == WOLFSSL_EXPORT_TLS) { + /* Encrypt-Then-MAC state. Historically only serialized for TLS; export + * version 6 and later also serialize it for DTLS. */ + if (type == WOLFSSL_EXPORT_TLS || ver > WOLFSSL_EXPORT_VERSION_5) { #ifdef HAVE_ENCRYPT_THEN_MAC exp[idx++] = options->disallowEncThenMac; exp[idx++] = options->encThenMac; @@ -1502,6 +1504,13 @@ static int ImportOptions(WOLFSSL* ssl, const byte* exp, word32 len, byte ver, } break; + case WOLFSSL_EXPORT_VERSION_5: + if (len < DTLS_EXPORT_OPT_SZ_5) { + WOLFSSL_MSG("Sanity check on buffer size failed"); + return BAD_FUNC_ARG; + } + break; + case WOLFSSL_EXPORT_VERSION_4: if (len < DTLS_EXPORT_OPT_SZ_4) { WOLFSSL_MSG("Sanity check on buffer size failed"); @@ -1628,7 +1637,9 @@ static int ImportOptions(WOLFSSL* ssl, const byte* exp, word32 len, byte ver, options->acceptState = exp[idx++]; options->asyncState = exp[idx++]; - if (type == WOLFSSL_EXPORT_TLS) { + /* Encrypt-Then-MAC state. Historically only serialized for TLS; export + * version 6 and later also serialize it for DTLS. */ + if (type == WOLFSSL_EXPORT_TLS || ver > WOLFSSL_EXPORT_VERSION_5) { #ifdef HAVE_ENCRYPT_THEN_MAC options->disallowEncThenMac = exp[idx++]; options->encThenMac = exp[idx++]; @@ -1723,8 +1734,8 @@ static int ImportPeerInfo(WOLFSSL* ssl, const byte* buf, word32 len, byte ver) word16 port; char ip[MAX_EXPORT_IP]; - if (ver != WOLFSSL_EXPORT_VERSION && ver != WOLFSSL_EXPORT_VERSION_4 && - ver != WOLFSSL_EXPORT_VERSION_3) { + if (ver != WOLFSSL_EXPORT_VERSION && ver != WOLFSSL_EXPORT_VERSION_5 && + ver != WOLFSSL_EXPORT_VERSION_4 && ver != WOLFSSL_EXPORT_VERSION_3) { WOLFSSL_MSG("Export version not supported"); return BAD_FUNC_ARG; } @@ -1982,6 +1993,15 @@ int wolfSSL_session_import_internal(WOLFSSL* ssl, const unsigned char* buf, } break; + case WOLFSSL_EXPORT_VERSION_5: + if (type == WOLFSSL_EXPORT_DTLS) { + optSz = DTLS_EXPORT_OPT_SZ_5; + } + else { + optSz = TLS_EXPORT_OPT_SZ_5; + } + break; + case WOLFSSL_EXPORT_VERSION_4: if (type == WOLFSSL_EXPORT_DTLS) { optSz = DTLS_EXPORT_OPT_SZ_4; diff --git a/tests/api/test_dtls.c b/tests/api/test_dtls.c index 336f89257f..f70c82befb 100644 --- a/tests/api/test_dtls.c +++ b/tests/api/test_dtls.c @@ -3294,3 +3294,101 @@ int test_dtls_set_session_min_downgrade(void) #endif return EXPECT_RESULT(); } + +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && defined(WOLFSSL_DTLS) && \ + defined(WOLFSSL_SESSION_EXPORT) && defined(HAVE_ENCRYPT_THEN_MAC) && \ + !defined(WOLFSSL_AEAD_ONLY) && !defined(WOLFSSL_NO_TLS12) && \ + !defined(NO_RSA) && !defined(NO_AES) && defined(HAVE_AES_CBC) && \ + !defined(NO_SHA256) && defined(HAVE_ECC) +/* Dummy peer callbacks so the DTLS exporter/importer has peer information to + * work with (the library requires these unless built with + * WOLFSSL_SESSION_EXPORT_NOPEER). */ +static int test_dtls_export_etm_get_peer(WOLFSSL* ssl, char* ip, int* ipSz, + unsigned short* port, int* fam) +{ + (void)ssl; + ip[0] = -1; + *ipSz = 1; + *port = 1; + *fam = 2; + return 1; +} + +static int test_dtls_export_etm_set_peer(WOLFSSL* ssl, char* ip, int ipSz, + unsigned short port, int fam) +{ + (void)ssl; + if (ip[0] != -1 || ipSz != 1 || port != 1 || fam != 2) + return 0; + return 1; +} +#endif + +/* Regression test for DTLS session export/import dropping the Encrypt-Then-MAC + * options. Historically the ETM option fields were only serialized for TLS, so + * a re-imported DTLS session lost the negotiated ETM state and broke the record + * layer. Establish a DTLS 1.2 connection with a CBC cipher suite (where ETM + * applies), export the session, re-import it into a fresh WOLFSSL, and confirm + * the ETM option fields survive the round trip. */ +int test_dtls12_export_import_etm(void) +{ + EXPECT_DECLS; +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && defined(WOLFSSL_DTLS) && \ + defined(WOLFSSL_SESSION_EXPORT) && defined(HAVE_ENCRYPT_THEN_MAC) && \ + !defined(WOLFSSL_AEAD_ONLY) && !defined(WOLFSSL_NO_TLS12) && \ + !defined(NO_RSA) && !defined(NO_AES) && defined(HAVE_AES_CBC) && \ + !defined(NO_SHA256) && defined(HAVE_ECC) + /* TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256 - a CBC suite, where ETM applies. */ + const char* cbcSuite = "ECDHE-RSA-AES128-SHA256"; + WOLFSSL_CTX *ctx_c = NULL, *ctx_s = NULL; + WOLFSSL *ssl_c = NULL, *ssl_s = NULL; + WOLFSSL *ssl_imp = NULL; + struct test_memio_ctx test_ctx; + unsigned char* session = NULL; + unsigned int sessionSz = 0; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfDTLSv1_2_client_method, wolfDTLSv1_2_server_method), 0); + ExpectIntEQ(wolfSSL_set_cipher_list(ssl_c, cbcSuite), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_set_cipher_list(ssl_s, cbcSuite), WOLFSSL_SUCCESS); + + /* The exporter/importer needs peer info callbacks. */ + wolfSSL_CTX_SetIOGetPeer(ctx_s, test_dtls_export_etm_get_peer); + wolfSSL_CTX_SetIOSetPeer(ctx_s, test_dtls_export_etm_set_peer); + + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + + /* Sanity: the handshake itself negotiated ETM on both sides. */ + if (ssl_c != NULL) + ExpectIntEQ(ssl_c->options.encThenMac, 1); + if (ssl_s != NULL) + ExpectIntEQ(ssl_s->options.encThenMac, 1); + + /* Export the server's DTLS session. */ + ExpectIntGE(wolfSSL_dtls_export(ssl_s, NULL, &sessionSz), 0); + ExpectIntGT(sessionSz, 0); + ExpectNotNull(session = (unsigned char*)XMALLOC(sessionSz, NULL, + DYNAMIC_TYPE_TMP_BUFFER)); + ExpectIntGE(wolfSSL_dtls_export(ssl_s, session, &sessionSz), 0); + + /* Import into a fresh WOLFSSL and confirm the ETM state survived. */ + ExpectNotNull(ssl_imp = wolfSSL_new(ctx_s)); + ExpectIntGE(wolfSSL_dtls_import(ssl_imp, session, sessionSz), 0); + if (ssl_imp != NULL) { + /* Regression check: pre-fix these were all reset to 0 for DTLS. */ + ExpectIntEQ(ssl_imp->options.encThenMac, 1); + ExpectIntEQ(ssl_imp->options.startedETMRead, 1); + ExpectIntEQ(ssl_imp->options.startedETMWrite, 1); + ExpectIntEQ(ssl_imp->options.disallowEncThenMac, 0); + } + + XFREE(session, NULL, DYNAMIC_TYPE_TMP_BUFFER); + wolfSSL_free(ssl_imp); + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} diff --git a/tests/api/test_dtls.h b/tests/api/test_dtls.h index 96e524b537..b7d81d1afd 100644 --- a/tests/api/test_dtls.h +++ b/tests/api/test_dtls.h @@ -58,6 +58,7 @@ int test_dtls13_no_session_id_echo(void); int test_dtls13_5_9_0_compat(void); int test_dtls13_oversized_cert_chain(void); int test_dtls_set_session_min_downgrade(void); +int test_dtls12_export_import_etm(void); #define TEST_DTLS_DECLS \ TEST_DECL_GROUP("dtls", test_dtls12_basic_connection_id), \ @@ -95,5 +96,6 @@ int test_dtls_set_session_min_downgrade(void); TEST_DECL_GROUP("dtls", test_dtls13_no_session_id_echo), \ TEST_DECL_GROUP("dtls", test_dtls13_oversized_cert_chain), \ TEST_DECL_GROUP("dtls", test_dtls_set_session_min_downgrade), \ - TEST_DECL_GROUP("dtls", test_dtls13_5_9_0_compat) + TEST_DECL_GROUP("dtls", test_dtls13_5_9_0_compat), \ + TEST_DECL_GROUP("dtls", test_dtls12_export_import_etm) #endif /* TESTS_API_DTLS_H */ diff --git a/wolfssl/internal.h b/wolfssl/internal.h index 025878a8d3..b67e3516ce 100644 --- a/wolfssl/internal.h +++ b/wolfssl/internal.h @@ -1632,25 +1632,28 @@ enum Misc { DTLS_EXPORT_PRO = 165,/* wolfSSL protocol for serialized session */ DTLS_EXPORT_STATE_PRO = 166,/* wolfSSL protocol for serialized state */ TLS_EXPORT_PRO = 167,/* wolfSSL protocol for serialized TLS */ - DTLS_EXPORT_OPT_SZ = 62, /* amount of bytes used from Options */ - DTLS_EXPORT_OPT_SZ_4 = 61, /* amount of bytes used from Options */ - TLS_EXPORT_OPT_SZ = 66, /* amount of bytes used from Options */ - TLS_EXPORT_OPT_SZ_4 = 65, /* amount of bytes used from Options */ - DTLS_EXPORT_OPT_SZ_3 = 60, /* amount of bytes used from Options */ + DTLS_EXPORT_OPT_SZ = 66, /* number of bytes used from Options */ + DTLS_EXPORT_OPT_SZ_5 = 62, /* number of bytes used from Options */ + DTLS_EXPORT_OPT_SZ_4 = 61, /* number of bytes used from Options */ + TLS_EXPORT_OPT_SZ = 66, /* number of bytes used from Options */ + TLS_EXPORT_OPT_SZ_5 = 66, /* number of bytes used from Options */ + TLS_EXPORT_OPT_SZ_4 = 65, /* number of bytes used from Options */ + DTLS_EXPORT_OPT_SZ_3 = 60, /* number of bytes used from Options */ DTLS_EXPORT_KEY_SZ = 325 + (DTLS_SEQ_SZ * 2), - /* max amount of bytes used from Keys */ + /* max number of bytes used from Keys */ DTLS_EXPORT_MIN_KEY_SZ = 85 + (DTLS_SEQ_SZ * 2), - /* min amount of bytes used from Keys */ + /* min number of bytes used from Keys */ WOLFSSL_EXPORT_TLS = 1, WOLFSSL_EXPORT_DTLS = 0, #ifndef WOLFSSL_EXPORT_SPC_SZ - WOLFSSL_EXPORT_SPC_SZ = 16, /* amount of bytes used from CipherSpecs */ + WOLFSSL_EXPORT_SPC_SZ = 16, /* number of bytes used from CipherSpecs */ #endif WOLFSSL_EXPORT_LEN = 2, /* 2 bytes for length and protocol */ - WOLFSSL_EXPORT_VERSION = 5, /* wolfSSL version for serialized session */ + WOLFSSL_EXPORT_VERSION = 6, /* wolfSSL version for serialized session */ - WOLFSSL_EXPORT_VERSION_4 = 4, /* 5.6.4 release and before */ /* older export versions supported */ + WOLFSSL_EXPORT_VERSION_5 = 5, /* version before DTLS Encrypt-Then-MAC */ + WOLFSSL_EXPORT_VERSION_4 = 4, /* 5.6.4 release and before */ WOLFSSL_EXPORT_VERSION_3 = 3, /* wolfSSL version before TLS 1.3 addition */ MAX_EXPORT_IP = 46, /* max ip size IPv4 mapped IPv6 */