From d9913ace348235c4fc3e1d4b92eaf18182f7a2e7 Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Tue, 9 Jun 2026 13:36:49 +0200 Subject: [PATCH 1/4] Add SSL_CTX_add_client_custom_ext (OpenSSL-compat client custom extensions) Implements the OpenSSL-compatible legacy client custom extension API for TLS 1.2 and below, mirroring SSL_CTX_add_client_custom_ext(). Application- defined extensions carry arbitrary IANA types, which cannot live in the TLSX list (it keys every extension on a fixed semaphore index that an arbitrary type would overrun), so they are kept in a separate list on the WOLFSSL_CTX and processed alongside the unknown-extension handling, like OpenSSL's custext. Gated on HAVE_TLS_EXTENSIONS && OPENSSL_EXTRA. Registration (wolfSSL_CTX_add_client_custom_ext): - Validates ext_type <= 0xffff, no free_cb without add_cb, not a type wolfSSL handles internally (incl. ech_outer_extensions), no duplicates. ClientHello (send): - TLSX_GetRequestSize runs each add_cb, serializes the wire bytes into a per-connection cache and calls free_cb; TLSX_WriteRequest copies them out. Honors add_cb returns 1/0/-1 (+alert). free_cb runs only after add_cb returns 1, and through a single cleanup path. The extension is always offered regardless of the client's max version (matching OpenSSL, whose is_tls13 check is false while building the ClientHello), so it works with flexible client methods that negotiate down to TLS 1.2. - Guards against word16 wire-field and extensions-block overflow. ServerHello (parse), matching ssl/statem/extensions_cust.c: - Unsolicited: a custom extension whose type was not emitted in our ClientHello (tracked in ssl->customExtSent) is rejected with unsupported_extension (RFC 5246 7.4.1.4 / SSL_EXT_FLAG_SENT). - Resumption: like SSL_EXT_IGNORE_ON_RESUMPTION, a server echo is ignored only on server-confirmed resumption -- resumption attempted AND the server echoed our session ID (RFC 5246, RFC 5077). A full-handshake fallback still parses/validates and rejects an unsolicited extension. - Duplicates: the semaphore-based detection cannot cover arbitrary types, so scan the already-parsed portion for an earlier extension of the same registered type and abort with DUPLICATE_TLS_EXT_E. - TLS 1.3 is left to RFC 8446 unsupported_extension handling; the legacy API is TLS 1.2-and-below only, matching OpenSSL's SSL_EXT_TLS1_2_AND_BELOW_ONLY. Plumbing: - Public API and custom_ext_*_cb typedefs in ssl.h, OpenSSL-compat mapping in openssl/ssl.h, both gated on OPENSSL_EXTRA && HAVE_TLS_EXTENSIONS. - TLSX_CustomExt_BuildRequest exported WOLFSSL_TEST_VIS for unit tests. - Lists/buffers freed in SSL_CtxResourceFree and SSL_ResourceFree. Adds unit tests covering registration validation, NULL-argument paths, TLS 1.2 / flexible-method / TLS 1.3 handshakes, ServerHello parse dispatch, and the unsolicited, duplicate, and resumption (ignore and fallback) cases. Builds with and without OPENSSL_EXTRA; full API suite passes. --- src/internal.c | 12 + src/tls.c | 426 ++++++++++++++++++++++++++++++++- tests/api.c | 11 + tests/api/test_tls_ext.c | 496 +++++++++++++++++++++++++++++++++++++++ tests/api/test_tls_ext.h | 13 +- wolfssl/internal.h | 40 ++++ wolfssl/openssl/ssl.h | 8 + wolfssl/ssl.h | 28 +++ 8 files changed, 1030 insertions(+), 4 deletions(-) diff --git a/src/internal.c b/src/internal.c index a14d59ff000..49c75b3f18a 100644 --- a/src/internal.c +++ b/src/internal.c @@ -3088,6 +3088,10 @@ void SSL_CtxResourceFree(WOLFSSL_CTX* ctx) #ifdef HAVE_TLS_EXTENSIONS #if !defined(NO_TLS) TLSX_FreeAll(ctx->extensions, ctx->heap); +#ifdef OPENSSL_EXTRA + TLSX_CustomExt_FreeAll(ctx->customExt, ctx->heap); + ctx->customExt = NULL; +#endif #endif /* !NO_TLS */ #ifndef NO_WOLFSSL_SERVER #if defined(HAVE_CERTIFICATE_STATUS_REQUEST) \ @@ -9050,6 +9054,14 @@ static void FreeSSL_Extensions(WOLFSSL* ssl) #if !defined(NO_TLS) TLSX_FreeAll(ssl->extensions, ssl->heap); ssl->extensions = NULL; +#ifdef OPENSSL_EXTRA + XFREE(ssl->customExtData, ssl->heap, DYNAMIC_TYPE_TMP_BUFFER); + ssl->customExtData = NULL; + ssl->customExtSz = 0; + XFREE(ssl->customExtSent, ssl->heap, DYNAMIC_TYPE_TLSX); + ssl->customExtSent = NULL; + ssl->customExtSentCnt = 0; +#endif #if defined(HAVE_SECURE_RENEGOTIATION) \ || defined(HAVE_SERVER_RENEGOTIATION_INFO) ssl->secure_renegotiation = NULL; diff --git a/src/tls.c b/src/tls.c index 59a9b2f373d..d6d0b46ac89 100644 --- a/src/tls.c +++ b/src/tls.c @@ -1555,6 +1555,13 @@ int wolfSSL_GetHmacType_ex(CipherSpecs* specs) /** Supports up to 72 flags. Increase as needed. */ #define SEMAPHORE_SIZE 9 +/** Highest extension type that TLSX_ToSemaphore() maps directly onto its own + * semaphore index. Higher types are either remapped into the remaining indices + * (renegotiation_info, QUIC, ECH, CKS) or fall outside the semaphore's range. + * This boundary also drives duplicate-extension detection in TLSX_Parse(); keep + * the two in sync. */ +#define SEMAPHORE_MAX_DIRECT_TYPE 62 + /** * Converts the extension type (id) to an index in the semaphore. * @@ -1574,7 +1581,8 @@ int wolfSSL_GetHmacType_ex(CipherSpecs* specs) * available semaphores, check for a possible collision with with a * 'remapped' extension type. * - * Update TLSX_Parse for duplicate detection if more added above 62. + * Update TLSX_Parse for duplicate detection if more added above + * SEMAPHORE_MAX_DIRECT_TYPE. */ static WC_INLINE word16 TLSX_ToSemaphore(word16 type) { @@ -1595,7 +1603,7 @@ static WC_INLINE word16 TLSX_ToSemaphore(word16 type) return 66; #endif default: - if (type > 62) { + if (type > SEMAPHORE_MAX_DIRECT_TYPE) { /* This message SHOULD only happens during the adding of new TLS extensions in which its IANA number overflows the current semaphore's range, or if its number already @@ -16968,6 +16976,347 @@ static int TLSX_GetSizeWithEch(WOLFSSL* ssl, byte* semaphore, byte msgType, } #endif +#if defined(HAVE_TLS_EXTENSIONS) && defined(OPENSSL_EXTRA) +/* OpenSSL-compatible application-defined ("custom") TLS extensions. + * + * Unlike the standard extensions above, custom extensions carry arbitrary + * IANA types chosen by the application, so they cannot live in the TLSX list + * (which keys every extension on a fixed semaphore index). They are kept in a + * separate list on the WOLFSSL_CTX and processed alongside the unknown + * extension handling. Only the client side, for TLS 1.2 and below, is wired up + * here, matching the legacy SSL_CTX_add_client_custom_ext() contract. */ + +/* Returns 1 if ext_type is an extension wolfSSL handles internally, which the + * application is therefore not allowed to register a custom handler for. */ +static int TLSX_CustomExt_IsKnown(word16 ext_type) +{ + switch (ext_type) { + case TLSXT_SERVER_NAME: + case TLSXT_MAX_FRAGMENT_LENGTH: + case TLSXT_TRUSTED_CA_KEYS: + case TLSXT_TRUNCATED_HMAC: + case TLSXT_STATUS_REQUEST: + case TLSXT_SUPPORTED_GROUPS: + case TLSXT_EC_POINT_FORMATS: + case TLSXT_SIGNATURE_ALGORITHMS: + case TLSXT_USE_SRTP: + case TLSXT_APPLICATION_LAYER_PROTOCOL: + case TLSXT_STATUS_REQUEST_V2: + case TLSXT_CLIENT_CERTIFICATE: + case TLSXT_SERVER_CERTIFICATE: + case TLSXT_ENCRYPT_THEN_MAC: + case TLSXT_EXTENDED_MASTER_SECRET: + case TLSXT_CERT_WITH_EXTERN_PSK: + case TLSXT_SESSION_TICKET: + case TLSXT_PRE_SHARED_KEY: + case TLSXT_EARLY_DATA: + case TLSXT_SUPPORTED_VERSIONS: + case TLSXT_COOKIE: + case TLSXT_PSK_KEY_EXCHANGE_MODES: + case TLSXT_CERTIFICATE_AUTHORITIES: + case TLSXT_POST_HANDSHAKE_AUTH: + case TLSXT_SIGNATURE_ALGORITHMS_CERT: + case TLSXT_KEY_SHARE: + case TLSXT_CONNECTION_ID: + case TLSXT_KEY_QUIC_TP_PARAMS: + case TLSXT_ECH: + case TLSXT_ECH_OUTER_EXTENSIONS: + case TLSXT_CKS: + case TLSXT_RENEGOTIATION_INFO: + case TLSXT_KEY_QUIC_TP_PARAMS_DRAFT: + return 1; + default: + return 0; + } +} + +/** + * Registers an application-defined client extension on the context. Mirrors + * OpenSSL's SSL_CTX_add_client_custom_ext(): returns WOLFSSL_SUCCESS (1) on + * success, WOLFSSL_FAILURE (0) on failure. + * + * In this legacy API, wolfSSL supports custom extensions on the client side + * for TLS 1.2 and below. + * + * @param ctx Context on which to register the custom extension. + * @param ext_type IANA extension type to register. Must fit in 16 bits, + * must not name an extension wolfSSL already handles + * internally, and must not already be registered on @p ctx. + * @param add_cb Callback used to build the outgoing extension. If NULL, a + * zero-length extension is sent. + * @param free_cb Optional callback used to release data produced by + * @p add_cb. Must be NULL when @p add_cb is NULL. + * @param add_arg Opaque application pointer for @p add_cb and @p free_cb. + * @param parse_cb Optional callback used to parse the echoed extension. + * @param parse_arg Opaque application pointer for @p parse_cb. + * @return WOLFSSL_SUCCESS on successful registration, otherwise + * WOLFSSL_FAILURE. + */ +int wolfSSL_CTX_add_client_custom_ext(WOLFSSL_CTX* ctx, unsigned int ext_type, + wolfSSL_custom_ext_add_cb add_cb, wolfSSL_custom_ext_free_cb free_cb, + void* add_arg, wolfSSL_custom_ext_parse_cb parse_cb, void* parse_arg) +{ + WOLFSSL_CustomExt* meth; + + WOLFSSL_ENTER("wolfSSL_CTX_add_client_custom_ext"); + + if (ctx == NULL || ext_type > 0xffff) + return WOLFSSL_FAILURE; + + /* free_cb without add_cb is meaningless: there is nothing to free. */ + if (add_cb == NULL && free_cb != NULL) + return WOLFSSL_FAILURE; + + /* Don't allow shadowing of internally handled extensions. */ + if (TLSX_CustomExt_IsKnown((word16)ext_type)) + return WOLFSSL_FAILURE; + + /* Reject duplicate registrations for the same type. */ + for (meth = ctx->customExt; meth != NULL; meth = meth->next) { + if (meth->ext_type == (word16)ext_type) + return WOLFSSL_FAILURE; + } + + meth = (WOLFSSL_CustomExt*)XMALLOC(sizeof(WOLFSSL_CustomExt), ctx->heap, + DYNAMIC_TYPE_TLSX); + if (meth == NULL) + return WOLFSSL_FAILURE; + + meth->ext_type = (word16)ext_type; + meth->add_cb = add_cb; + meth->free_cb = free_cb; + meth->parse_cb = parse_cb; + meth->add_arg = add_arg; + meth->parse_arg = parse_arg; + meth->next = ctx->customExt; + ctx->customExt = meth; + + return WOLFSSL_SUCCESS; +} + +/* Frees a list of registered custom extension methods. */ +void TLSX_CustomExt_FreeAll(WOLFSSL_CustomExt* list, void* heap) +{ + WOLFSSL_CustomExt* meth; + + while ((meth = list) != NULL) { + list = meth->next; + XFREE(meth, heap, DYNAMIC_TYPE_TLSX); + } +} + +/* Invokes the registered add callbacks and serializes the resulting custom + * extensions for the ClientHello into ssl->customExtData. The total wire size + * (type + length + data for each included extension) is returned in *pSz. The + * buffer is consumed and released by TLSX_WriteRequest. */ +WOLFSSL_TEST_VIS int TLSX_CustomExt_BuildRequest(WOLFSSL* ssl, word16* pSz) +{ + WOLFSSL_CustomExt* meth; + byte* data = NULL; + word32 dataSz = 0; /* word32 to detect a word16 wire-field overflow */ + int ret = 0; + + if (ssl == NULL || pSz == NULL) + return BAD_FUNC_ARG; + + *pSz = 0; + + XFREE(ssl->customExtData, ssl->heap, DYNAMIC_TYPE_TMP_BUFFER); + ssl->customExtData = NULL; + ssl->customExtSz = 0; + XFREE(ssl->customExtSent, ssl->heap, DYNAMIC_TYPE_TLSX); + ssl->customExtSent = NULL; + ssl->customExtSentCnt = 0; + + if (ssl->ctx == NULL || ssl->ctx->customExt == NULL) + return 0; + + for (meth = ssl->ctx->customExt; meth != NULL && ret == 0; + meth = meth->next) { + const unsigned char* out = NULL; + size_t outlen = 0; + int al = unsupported_extension; + int addRet = 1; /* no add_cb => add a zero-length extension */ + word32 need = 0; + byte* tmp = NULL; + word16* sent = NULL; + + if (meth->add_cb != NULL) { + addRet = meth->add_cb(ssl, meth->ext_type, &out, &outlen, &al, + meth->add_arg); + } + + if (addRet < 0) { + /* Fatal: callback requested the connection be aborted. add_cb + * returned < 0, so free_cb is not run (skips free_ext). */ + SendAlert(ssl, alert_fatal, (byte)al); + ret = WOLFSSL_FATAL_ERROR; + break; + } + if (addRet == 0) + continue; /* extension omitted for this message */ + + if (out == NULL && outlen > 0) { + ret = BAD_FUNC_ARG; + } + else if (outlen > WOLFSSL_MAX_16BIT) { + ret = BUFFER_ERROR; + } + else { + need = HELLO_EXT_TYPE_SZ + OPAQUE16_LEN + (word32)outlen; + if (dataSz + need > (word32)WOLFSSL_MAX_16BIT) + ret = BUFFER_ERROR; + } + if (ret != 0) + goto free_ext; + + tmp = (byte*)XREALLOC(data, dataSz + need, ssl->heap, + DYNAMIC_TYPE_TMP_BUFFER); + if (tmp == NULL) { + ret = MEMORY_E; + goto free_ext; + } + data = tmp; + + c16toa(meth->ext_type, data + dataSz); + dataSz += HELLO_EXT_TYPE_SZ; + c16toa((word16)outlen, data + dataSz); + dataSz += OPAQUE16_LEN; + if (outlen > 0) { + XMEMCPY(data + dataSz, out, outlen); + dataSz += (word32)outlen; + } + + /* Record the type as sent so the server may legitimately echo it. */ + sent = (word16*)XREALLOC(ssl->customExtSent, + (ssl->customExtSentCnt + 1) * (word32)sizeof(word16), + ssl->heap, DYNAMIC_TYPE_TLSX); + if (sent == NULL) { + ret = MEMORY_E; + goto free_ext; + } + ssl->customExtSent = sent; + ssl->customExtSent[ssl->customExtSentCnt++] = meth->ext_type; + +free_ext: + if (meth->free_cb != NULL) + meth->free_cb(ssl, meth->ext_type, out, meth->add_arg); + } + + if (ret != 0) { + XFREE(data, ssl->heap, DYNAMIC_TYPE_TMP_BUFFER); + if (ssl->customExtSent != NULL) { + XFREE(ssl->customExtSent, ssl->heap, DYNAMIC_TYPE_TLSX); + ssl->customExtSent = NULL; + ssl->customExtSentCnt = 0; + } + return ret; + } + + ssl->customExtData = data; + ssl->customExtSz = (word16)dataSz; + *pSz = (word16)dataSz; + + return 0; +} + +/* Returns 1 if a custom extension handler is registered for the given type. */ +static int TLSX_CustomExt_IsRegistered(const WOLFSSL* ssl, word16 type) +{ + WOLFSSL_CustomExt* meth; + + if (ssl->ctx == NULL) + return 0; + for (meth = ssl->ctx->customExt; meth != NULL; meth = meth->next) { + if (meth->ext_type == type) + return 1; + } + return 0; +} + +/* Returns 1 if the given custom extension type was emitted in our ClientHello. */ +static int TLSX_CustomExt_WasSent(const WOLFSSL* ssl, word16 type) +{ + word16 i; + + for (i = 0; i < ssl->customExtSentCnt; i++) { + if (ssl->customExtSent[i] == type) + return 1; + } + return 0; +} + +/* Looks up a registered custom extension matching the received type and, if + * found, invokes its parse callback. *found is set to 1 when a handler matched + * (whether it succeeded or failed). Returns 0 on success, or a negative error + * (after sending the appropriate alert) when the extension is unsolicited or + * the callback rejects the data. */ +int TLSX_CustomExt_Parse(WOLFSSL* ssl, byte msgType, word16 type, + const byte* input, word16 size, int* found) +{ + WOLFSSL_CustomExt* meth; + + *found = 0; + + if (ssl->ctx == NULL || ssl->ctx->customExt == NULL) + return 0; + + /* Legacy client custom extensions only apply to the ServerHello of a + * TLS 1.2 (or below) handshake. For TLS 1.3, fall through so the unknown + * extension is handled per RFC 8446 (unsupported_extension alert). */ + if (msgType != server_hello || IsAtLeastTLSv1_3(ssl->version)) + return 0; + + /* OpenSSL registers the legacy API with SSL_EXT_IGNORE_ON_RESUMPTION, so on + * a resumed handshake the extension is not processed (the server echo is + * silently ignored). Only ignore when the server has actually confirmed + * resumption by echoing our session ID -- the RFC 5246 / RFC 5077 (tickets, + * non-empty session ID) signal. A cached ticket alone is not enough: if the + * server falls back to a full handshake it will not echo our session ID, so + * the extension is still parsed/validated below (and an unsolicited one is + * rejected). These fields are set from the ServerHello before this point. */ + if (ssl->options.resuming && ssl->options.haveSessionId && + ssl->arrays != NULL && ssl->session != NULL && + ssl->arrays->sessionIDSz > 0 && + ssl->arrays->sessionIDSz == ssl->session->sessionIDSz && + XMEMCMP(ssl->arrays->sessionID, ssl->session->sessionID, + ssl->arrays->sessionIDSz) == 0) { + return 0; + } + + for (meth = ssl->ctx->customExt; meth != NULL; meth = meth->next) { + if (meth->ext_type != type) + continue; + + *found = 1; + + /* RFC 5246 7.4.1.4: the server must not send an extension the client + * did not request. add_cb may decline to send for a given handshake, + * so reject a response for any type we did not actually emit. */ + if (!TLSX_CustomExt_WasSent(ssl, type)) { + WOLFSSL_MSG("Unsolicited custom extension in ServerHello"); + SendAlert(ssl, alert_fatal, unsupported_extension); + WOLFSSL_ERROR_VERBOSE(UNSUPPORTED_EXTENSION); + return UNSUPPORTED_EXTENSION; + } + + if (meth->parse_cb != NULL) { + int al = unsupported_extension; + int parseRet = meth->parse_cb(ssl, type, input, (size_t)size, &al, + meth->parse_arg); + if (parseRet <= 0) { + SendAlert(ssl, alert_fatal, (byte)al); + WOLFSSL_ERROR_VERBOSE(UNSUPPORTED_EXTENSION); + return UNSUPPORTED_EXTENSION; + } + } + break; + } + + return 0; +} +#endif /* HAVE_TLS_EXTENSIONS && OPENSSL_EXTRA */ + /** Tells the buffered size of extensions to be sent into the client hello. */ int TLSX_GetRequestSize(WOLFSSL* ssl, byte msgType, word32* pLength) { @@ -17073,6 +17422,27 @@ int TLSX_GetRequestSize(WOLFSSL* ssl, byte msgType, word32* pLength) } #endif +#if defined(HAVE_TLS_EXTENSIONS) && defined(OPENSSL_EXTRA) + /* Application-defined (custom) extensions. These are always offered in the + * ClientHello regardless of the client's maximum version (matching OpenSSL, + * whose is_tls13 check is false while constructing the ClientHello), so + * they work with flexible client methods that go on to negotiate TLS 1.2. + * The negotiated-version restriction is enforced on the parse side. The add + * callbacks run here and the resulting bytes are cached for + * TLSX_WriteRequest. */ + if (msgType == client_hello) { + word16 customSz = 0; + ret = TLSX_CustomExt_BuildRequest(ssl, &customSz); + if (ret != 0) + return ret; + if ((word32)length + customSz > (WOLFSSL_MAX_16BIT - OPAQUE16_LEN)) { + WOLFSSL_MSG("TLSX_GetRequestSize extensions exceed word16"); + return BUFFER_E; + } + length += customSz; + } +#endif + /* The TLS extensions block length prefix is a 2-byte field, so any * accumulated total above 0xFFFF must be rejected rather than silently * truncating and producing a short, malformed handshake message. */ @@ -17299,6 +17669,19 @@ int TLSX_WriteRequest(WOLFSSL* ssl, byte* output, byte msgType, word32* pOffset) } #endif +#if defined(HAVE_TLS_EXTENSIONS) && defined(OPENSSL_EXTRA) + /* Copy out the application-defined (custom) extension bytes built during + * TLSX_GetRequestSize, then release the cached buffer. */ + if (msgType == client_hello && ssl->customExtData != NULL) { + WOLFSSL_MSG("Custom extensions to write"); + XMEMCPY(output + offset, ssl->customExtData, ssl->customExtSz); + offset += ssl->customExtSz; + XFREE(ssl->customExtData, ssl->heap, DYNAMIC_TYPE_TMP_BUFFER); + ssl->customExtData = NULL; + ssl->customExtSz = 0; + } +#endif + #ifdef WOLFSSL_TLS13 #if defined(HAVE_SESSION_TICKET) || !defined(NO_PSK) if (msgType == client_hello && IsAtLeastTLSv1_3(ssl->version)) { @@ -17908,7 +18291,8 @@ WOLFSSL_TEST_VIS int TLSX_Parse(WOLFSSL* ssl, const byte* input, word16 length, offset += OPAQUE16_LEN; /* Check we have a bit for extension type. */ - if ((type <= 62) || (type == TLSX_RENEGOTIATION_INFO) + if ((type <= SEMAPHORE_MAX_DIRECT_TYPE) + || (type == TLSX_RENEGOTIATION_INFO) #ifdef WOLFSSL_QUIC || (type == TLSX_KEY_QUIC_TP_PARAMS_DRAFT) #endif @@ -17928,6 +18312,28 @@ WOLFSSL_TEST_VIS int TLSX_Parse(WOLFSSL* ssl, const byte* input, word16 length, return DUPLICATE_TLS_EXT_E; } } +#if defined(HAVE_TLS_EXTENSIONS) && defined(OPENSSL_EXTRA) + /* The semaphore-based duplicate detection above does not cover + * application-registered custom extensions whose arbitrary type is + * above the semaphore range. Match OpenSSL, which gives each registered + * custom extension its own slot and rejects repeats: scan the + * already-parsed portion of this message for an earlier extension of + * the same type. (Types <= SEMAPHORE_MAX_DIRECT_TYPE are handled by + * the block above.) */ + else if (type > SEMAPHORE_MAX_DIRECT_TYPE && + TLSX_CustomExt_IsRegistered(ssl, type)) { + word32 scan = 0; + word32 upto = (word32)offset - HELLO_EXT_TYPE_SZ - OPAQUE16_LEN; + while (scan + HELLO_EXT_TYPE_SZ + OPAQUE16_LEN <= upto) { + word16 sT, sS; + ato16(input + scan, &sT); + ato16(input + scan + HELLO_EXT_TYPE_SZ, &sS); + if (sT == type) + return DUPLICATE_TLS_EXT_E; + scan += HELLO_EXT_TYPE_SZ + OPAQUE16_LEN + sS; + } + } +#endif if (length - offset < size) return BUFFER_ERROR; @@ -18601,6 +19007,20 @@ WOLFSSL_TEST_VIS int TLSX_Parse(WOLFSSL* ssl, const byte* input, word16 length, return INVALID_PARAMETER; #endif default: +#if defined(HAVE_TLS_EXTENSIONS) && defined(OPENSSL_EXTRA) + { + /* Application-defined (custom) extension handler, if one + * was registered for this type. */ + int customFound = 0; + ret = TLSX_CustomExt_Parse(ssl, msgType, type, + input + offset, size, + &customFound); + if (ret != 0) + return ret; + if (customFound) + break; + } +#endif WOLFSSL_MSG("Unknown TLS extension type"); #if defined(WOLFSSL_TLS13) /* RFC 8446 Sec. 4.2: a TLS 1.3 client MUST abort with an diff --git a/tests/api.c b/tests/api.c index 957dd445b3b..7e3e9739a4a 100644 --- a/tests/api.c +++ b/tests/api.c @@ -36210,6 +36210,17 @@ TEST_CASE testCases[] = { TEST_DECL(test_TLSX_ALPN_server_response_count), TEST_DECL(test_TLSX_SupportedCurve_empty_or_unsupported), TEST_DECL(test_TLSX_PointFormat_uncompressed_required), + TEST_DECL(test_wolfSSL_CTX_add_client_custom_ext), + TEST_DECL(test_wolfSSL_custom_ext_handshake), + TEST_DECL(test_wolfSSL_custom_ext_flexible_handshake), + TEST_DECL(test_wolfSSL_custom_ext_tls13_handshake), + TEST_DECL(test_wolfSSL_custom_ext_parse), + TEST_DECL(test_wolfSSL_custom_ext_unsolicited), + TEST_DECL(test_wolfSSL_custom_ext_duplicate), + TEST_DECL(test_wolfSSL_custom_ext_resumption_ignored), + TEST_DECL(test_wolfSSL_custom_ext_resumption_fallback), + TEST_DECL(test_wolfSSL_custom_ext_ticket_fallback), + TEST_DECL(test_wolfSSL_custom_ext_add_null), TEST_DECL(test_wolfSSL_wolfSSL_UseSecureRenegotiation), TEST_DECL(test_wolfSSL_clear_secure_renegotiation), TEST_DECL(test_wolfSSL_SCR_Reconnect), diff --git a/tests/api/test_tls_ext.c b/tests/api/test_tls_ext.c index 34d95f12125..6453315505f 100644 --- a/tests/api/test_tls_ext.c +++ b/tests/api/test_tls_ext.c @@ -1310,6 +1310,502 @@ int test_TLSX_ECH_msg_type_validation(void) return EXPECT_RESULT(); } +/* ---- Application-defined ("custom") client extensions ------------------- */ +#if defined(OPENSSL_EXTRA) && defined(HAVE_TLS_EXTENSIONS) && \ + !defined(NO_TLS) && !defined(NO_WOLFSSL_CLIENT) && \ + (!defined(WOLFSSL_NO_TLS12) || \ + (defined(WOLFSSL_TLS13) && defined(HAVE_SSL_MEMIO_TESTS_DEPENDENCIES))) + +#define TEST_CUSTOM_EXT_TYPE 0x4242 +static int test_custom_add_arg_obj; +static int test_custom_parse_arg_obj; +#define TEST_CUSTOM_ADD_ARG ((void*)&test_custom_add_arg_obj) +#define TEST_CUSTOM_PARSE_ARG ((void*)&test_custom_parse_arg_obj) + +static const unsigned char test_custom_ext_data[] = { 'w', 'o', 'l', 'f' }; + +static int test_custom_ext_add_called; +static int test_custom_ext_free_called; +static int test_custom_ext_parse_called; +static int test_custom_ext_add_bad; +static int test_custom_ext_parse_bad; + +static int test_custom_ext_add_cb(WOLFSSL* ssl, unsigned int ext_type, + const unsigned char** out, size_t* outlen, int* al, void* add_arg) +{ + (void)ssl; + (void)al; + test_custom_ext_add_called++; + if (ext_type != TEST_CUSTOM_EXT_TYPE || add_arg != TEST_CUSTOM_ADD_ARG) + test_custom_ext_add_bad++; + *out = test_custom_ext_data; + *outlen = sizeof(test_custom_ext_data); + return 1; +} + +static void test_custom_ext_free_cb(WOLFSSL* ssl, unsigned int ext_type, + const unsigned char* out, void* add_arg) +{ + (void)ssl; + (void)ext_type; + (void)out; + (void)add_arg; + test_custom_ext_free_called++; +} + +static int test_custom_ext_parse_cb(WOLFSSL* ssl, unsigned int ext_type, + const unsigned char* in, size_t inlen, int* al, void* parse_arg) +{ + (void)ssl; + (void)al; + test_custom_ext_parse_called++; + if (ext_type != TEST_CUSTOM_EXT_TYPE || parse_arg != TEST_CUSTOM_PARSE_ARG) + test_custom_ext_parse_bad++; + if (inlen != sizeof(test_custom_ext_data) || + XMEMCMP(in, test_custom_ext_data, inlen) != 0) + test_custom_ext_parse_bad++; + return 1; +} + +#ifndef WOLFSSL_NO_TLS12 +static int test_custom_ext_add_null_cb(WOLFSSL* ssl, unsigned int ext_type, + const unsigned char** out, size_t* outlen, int* al, void* add_arg) +{ + (void)ssl; + (void)ext_type; + (void)al; + (void)add_arg; + *out = NULL; + *outlen = 4; + return 1; +} +#endif /* !WOLFSSL_NO_TLS12 */ + +static void test_custom_ext_reset(void) +{ + test_custom_ext_add_called = 0; + test_custom_ext_free_called = 0; + test_custom_ext_parse_called = 0; + test_custom_ext_add_bad = 0; + test_custom_ext_parse_bad = 0; +} + +#ifdef HAVE_SSL_MEMIO_TESTS_DEPENDENCIES +static int test_custom_ext_handshake_ctx_ready(WOLFSSL_CTX* ctx) +{ + EXPECT_DECLS; + ExpectIntEQ(wolfSSL_CTX_add_client_custom_ext(ctx, TEST_CUSTOM_EXT_TYPE, + test_custom_ext_add_cb, test_custom_ext_free_cb, TEST_CUSTOM_ADD_ARG, + test_custom_ext_parse_cb, TEST_CUSTOM_PARSE_ARG), WOLFSSL_SUCCESS); + return EXPECT_RESULT(); +} +#endif +#endif + +/* Validates the registration contract of wolfSSL_CTX_add_client_custom_ext. */ +int test_wolfSSL_CTX_add_client_custom_ext(void) +{ + EXPECT_DECLS; +#if defined(OPENSSL_EXTRA) && defined(HAVE_TLS_EXTENSIONS) && \ + !defined(NO_TLS) && !defined(NO_WOLFSSL_CLIENT) && !defined(WOLFSSL_NO_TLS12) + WOLFSSL_CTX* ctx = NULL; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + + /* NULL ctx is rejected. */ + ExpectIntEQ(wolfSSL_CTX_add_client_custom_ext(NULL, TEST_CUSTOM_EXT_TYPE, + test_custom_ext_add_cb, test_custom_ext_free_cb, NULL, + test_custom_ext_parse_cb, NULL), 0); + + /* A type wolfSSL handles internally (server_name) is rejected. */ + ExpectIntEQ(wolfSSL_CTX_add_client_custom_ext(ctx, 0x0000, + test_custom_ext_add_cb, test_custom_ext_free_cb, NULL, + test_custom_ext_parse_cb, NULL), 0); + + /* free_cb without add_cb is rejected. */ + ExpectIntEQ(wolfSSL_CTX_add_client_custom_ext(ctx, TEST_CUSTOM_EXT_TYPE, + NULL, test_custom_ext_free_cb, NULL, + test_custom_ext_parse_cb, NULL), 0); + + /* ext_type larger than a 16-bit value is rejected. */ + ExpectIntEQ(wolfSSL_CTX_add_client_custom_ext(ctx, 0x10000, + test_custom_ext_add_cb, test_custom_ext_free_cb, NULL, + test_custom_ext_parse_cb, NULL), 0); + + /* A valid registration succeeds. */ + ExpectIntEQ(wolfSSL_CTX_add_client_custom_ext(ctx, TEST_CUSTOM_EXT_TYPE, + test_custom_ext_add_cb, test_custom_ext_free_cb, TEST_CUSTOM_ADD_ARG, + test_custom_ext_parse_cb, TEST_CUSTOM_PARSE_ARG), WOLFSSL_SUCCESS); + + /* Registering the same type twice is rejected. */ + ExpectIntEQ(wolfSSL_CTX_add_client_custom_ext(ctx, TEST_CUSTOM_EXT_TYPE, + test_custom_ext_add_cb, test_custom_ext_free_cb, NULL, + test_custom_ext_parse_cb, NULL), 0); + + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +/* TLSX_CustomExt_BuildRequest must reject NULL arguments, and an add_cb that + * returns a non-zero length with a NULL data pointer must fail cleanly + * (running free_cb) instead of crashing. */ +int test_wolfSSL_custom_ext_add_null(void) +{ + EXPECT_DECLS; +#if defined(OPENSSL_EXTRA) && defined(HAVE_TLS_EXTENSIONS) && \ + !defined(NO_TLS) && !defined(NO_WOLFSSL_CLIENT) && !defined(WOLFSSL_NO_TLS12) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + word16 builtSz = 0; + + test_custom_ext_reset(); + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + ExpectIntEQ(wolfSSL_CTX_add_client_custom_ext(ctx, TEST_CUSTOM_EXT_TYPE, + test_custom_ext_add_null_cb, test_custom_ext_free_cb, NULL, + NULL, NULL), WOLFSSL_SUCCESS); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + + ExpectIntEQ(TLSX_CustomExt_BuildRequest(NULL, &builtSz), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(TLSX_CustomExt_BuildRequest(ssl, NULL), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + + ExpectIntNE(TLSX_CustomExt_BuildRequest(ssl, &builtSz), 0); + ExpectIntEQ(test_custom_ext_free_called, 1); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +/* Drives a TLS 1.2 handshake and checks the client's custom-extension add and + * free callbacks run while building the ClientHello. */ +int test_wolfSSL_custom_ext_handshake(void) +{ + EXPECT_DECLS; +#if defined(OPENSSL_EXTRA) && defined(HAVE_TLS_EXTENSIONS) && \ + !defined(NO_TLS) && !defined(NO_WOLFSSL_CLIENT) && \ + !defined(WOLFSSL_NO_TLS12) && defined(HAVE_SSL_MEMIO_TESTS_DEPENDENCIES) + test_ssl_cbf client_cbf; + test_ssl_cbf server_cbf; + + XMEMSET(&client_cbf, 0, sizeof(client_cbf)); + XMEMSET(&server_cbf, 0, sizeof(server_cbf)); + test_custom_ext_reset(); + + client_cbf.method = wolfTLSv1_2_client_method; + client_cbf.ctx_ready = test_custom_ext_handshake_ctx_ready; + server_cbf.method = wolfTLSv1_2_server_method; + + ExpectIntEQ(test_wolfSSL_client_server_nofail_memio(&client_cbf, + &server_cbf, NULL), TEST_SUCCESS); + + /* add_cb must have run at least once, free_cb must balance it, and no + * callback observed an unexpected type or argument. */ + ExpectIntGE(test_custom_ext_add_called, 1); + ExpectIntEQ(test_custom_ext_free_called, test_custom_ext_add_called); + ExpectIntEQ(test_custom_ext_add_bad, 0); +#endif + return EXPECT_RESULT(); +} + +/* A flexible client method (whose pre-handshake version is the maximum, e.g. + * TLS 1.3) must still offer the legacy custom extension so it works when the + * connection negotiates TLS 1.2. */ +int test_wolfSSL_custom_ext_flexible_handshake(void) +{ + EXPECT_DECLS; +#if defined(OPENSSL_EXTRA) && defined(HAVE_TLS_EXTENSIONS) && \ + !defined(NO_TLS) && !defined(NO_WOLFSSL_CLIENT) && \ + !defined(WOLFSSL_NO_TLS12) && defined(HAVE_SSL_MEMIO_TESTS_DEPENDENCIES) + test_ssl_cbf client_cbf; + test_ssl_cbf server_cbf; + + XMEMSET(&client_cbf, 0, sizeof(client_cbf)); + XMEMSET(&server_cbf, 0, sizeof(server_cbf)); + test_custom_ext_reset(); + + /* Flexible client (offers up to its max version), server pinned to TLS 1.2 + * so the handshake negotiates down. */ + client_cbf.method = wolfSSLv23_client_method; + client_cbf.ctx_ready = test_custom_ext_handshake_ctx_ready; + server_cbf.method = wolfTLSv1_2_server_method; + + ExpectIntEQ(test_wolfSSL_client_server_nofail_memio(&client_cbf, + &server_cbf, NULL), TEST_SUCCESS); + + /* The extension was offered despite the client's pre-handshake max version + * being above TLS 1.2. */ + ExpectIntGE(test_custom_ext_add_called, 1); + ExpectIntEQ(test_custom_ext_free_called, test_custom_ext_add_called); + ExpectIntEQ(test_custom_ext_add_bad, 0); +#endif + return EXPECT_RESULT(); +} + +/* Offering the legacy custom extension in a ClientHello that negotiates TLS 1.3 + * must not break the handshake (the server ignores the unknown extension). */ +int test_wolfSSL_custom_ext_tls13_handshake(void) +{ + EXPECT_DECLS; +#if defined(OPENSSL_EXTRA) && defined(HAVE_TLS_EXTENSIONS) && \ + !defined(NO_TLS) && !defined(NO_WOLFSSL_CLIENT) && \ + defined(WOLFSSL_TLS13) && defined(HAVE_SSL_MEMIO_TESTS_DEPENDENCIES) + test_ssl_cbf client_cbf; + test_ssl_cbf server_cbf; + + XMEMSET(&client_cbf, 0, sizeof(client_cbf)); + XMEMSET(&server_cbf, 0, sizeof(server_cbf)); + test_custom_ext_reset(); + + client_cbf.method = wolfTLSv1_3_client_method; + client_cbf.ctx_ready = test_custom_ext_handshake_ctx_ready; + server_cbf.method = wolfTLSv1_3_server_method; + + ExpectIntEQ(test_wolfSSL_client_server_nofail_memio(&client_cbf, + &server_cbf, NULL), TEST_SUCCESS); + + /* Sent in the ClientHello; the TLS 1.3 server ignores it and never echoes + * it, so parse never runs. */ + ExpectIntGE(test_custom_ext_add_called, 1); + ExpectIntEQ(test_custom_ext_free_called, test_custom_ext_add_called); + ExpectIntEQ(test_custom_ext_parse_called, 0); +#endif + return EXPECT_RESULT(); +} + +/* Feeds a ServerHello extension matching a registered custom type and checks + * the parse callback is invoked with the right data. */ +int test_wolfSSL_custom_ext_parse(void) +{ + EXPECT_DECLS; +#if defined(OPENSSL_EXTRA) && defined(HAVE_TLS_EXTENSIONS) && \ + !defined(NO_TLS) && !defined(NO_WOLFSSL_CLIENT) && !defined(WOLFSSL_NO_TLS12) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + word16 builtSz = 0; + /* type = 0x4242, len = 0x0004, data = "wolf" */ + const byte extBytes[] = { 0x42, 0x42, 0x00, 0x04, 'w', 'o', 'l', 'f' }; + + test_custom_ext_reset(); + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + ExpectIntEQ(wolfSSL_CTX_add_client_custom_ext(ctx, TEST_CUSTOM_EXT_TYPE, + test_custom_ext_add_cb, test_custom_ext_free_cb, TEST_CUSTOM_ADD_ARG, + test_custom_ext_parse_cb, TEST_CUSTOM_PARSE_ARG), WOLFSSL_SUCCESS); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + + /* Emit the extension first (records it as sent) so the server's echo is + * accepted rather than rejected as unsolicited. */ + ExpectIntEQ(TLSX_CustomExt_BuildRequest(ssl, &builtSz), 0); + + ExpectIntEQ(TLSX_Parse(ssl, extBytes, (word16)sizeof(extBytes), + server_hello, NULL), 0); + ExpectIntEQ(test_custom_ext_parse_called, 1); + ExpectIntEQ(test_custom_ext_parse_bad, 0); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +/* A ServerHello extension whose type was never emitted in the ClientHello + * (add_cb declined) must be rejected as unsolicited, without invoking parse. */ +int test_wolfSSL_custom_ext_unsolicited(void) +{ + EXPECT_DECLS; +#if defined(OPENSSL_EXTRA) && defined(HAVE_TLS_EXTENSIONS) && \ + !defined(NO_TLS) && !defined(NO_WOLFSSL_CLIENT) && !defined(WOLFSSL_NO_TLS12) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + const byte extBytes[] = { 0x42, 0x42, 0x00, 0x04, 'w', 'o', 'l', 'f' }; + + test_custom_ext_reset(); + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + ExpectIntEQ(wolfSSL_CTX_add_client_custom_ext(ctx, TEST_CUSTOM_EXT_TYPE, + test_custom_ext_add_cb, test_custom_ext_free_cb, TEST_CUSTOM_ADD_ARG, + test_custom_ext_parse_cb, TEST_CUSTOM_PARSE_ARG), WOLFSSL_SUCCESS); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + + /* No BuildRequest: nothing was sent, so the echo is unsolicited. */ + ExpectIntEQ(TLSX_Parse(ssl, extBytes, (word16)sizeof(extBytes), + server_hello, NULL), + WC_NO_ERR_TRACE(UNSUPPORTED_EXTENSION)); + ExpectIntEQ(test_custom_ext_parse_called, 0); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +/* A ServerHello carrying the same registered custom extension twice must abort + * with DUPLICATE_TLS_EXT_E (matching the built-in duplicate handling). */ +int test_wolfSSL_custom_ext_duplicate(void) +{ + EXPECT_DECLS; +#if defined(OPENSSL_EXTRA) && defined(HAVE_TLS_EXTENSIONS) && \ + !defined(NO_TLS) && !defined(NO_WOLFSSL_CLIENT) && !defined(WOLFSSL_NO_TLS12) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + word16 builtSz = 0; + /* Two copies of type 0x4242. */ + const byte extBytes[] = { 0x42, 0x42, 0x00, 0x04, 'w', 'o', 'l', 'f', + 0x42, 0x42, 0x00, 0x04, 'w', 'o', 'l', 'f' }; + + test_custom_ext_reset(); + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + ExpectIntEQ(wolfSSL_CTX_add_client_custom_ext(ctx, TEST_CUSTOM_EXT_TYPE, + test_custom_ext_add_cb, test_custom_ext_free_cb, TEST_CUSTOM_ADD_ARG, + test_custom_ext_parse_cb, TEST_CUSTOM_PARSE_ARG), WOLFSSL_SUCCESS); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + + ExpectIntEQ(TLSX_CustomExt_BuildRequest(ssl, &builtSz), 0); + + ExpectIntEQ(TLSX_Parse(ssl, extBytes, (word16)sizeof(extBytes), + server_hello, NULL), + WC_NO_ERR_TRACE(DUPLICATE_TLS_EXT_E)); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +/* On a resumed handshake the legacy custom extension is ignored (OpenSSL's + * SSL_EXT_IGNORE_ON_RESUMPTION): a server echo neither invokes parse nor + * aborts. */ +int test_wolfSSL_custom_ext_resumption_ignored(void) +{ + EXPECT_DECLS; +#if defined(OPENSSL_EXTRA) && defined(HAVE_TLS_EXTENSIONS) && \ + !defined(NO_TLS) && !defined(NO_WOLFSSL_CLIENT) && !defined(WOLFSSL_NO_TLS12) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + word16 builtSz = 0; + const byte extBytes[] = { 0x42, 0x42, 0x00, 0x04, 'w', 'o', 'l', 'f' }; + + test_custom_ext_reset(); + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + ExpectIntEQ(wolfSSL_CTX_add_client_custom_ext(ctx, TEST_CUSTOM_EXT_TYPE, + test_custom_ext_add_cb, test_custom_ext_free_cb, TEST_CUSTOM_ADD_ARG, + test_custom_ext_parse_cb, TEST_CUSTOM_PARSE_ARG), WOLFSSL_SUCCESS); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + + ExpectIntEQ(TLSX_CustomExt_BuildRequest(ssl, &builtSz), 0); + + /* Simulate a confirmed resumption: the server echoes our session ID, the + * signal the parse gate keys on. */ + if (ssl != NULL && ssl->arrays != NULL && ssl->session != NULL) { + ssl->options.resuming = 1; + ssl->options.haveSessionId = 1; + ssl->arrays->sessionIDSz = ID_LEN; + ssl->session->sessionIDSz = ID_LEN; + XMEMSET(ssl->arrays->sessionID, 0xA5, ID_LEN); + XMEMSET(ssl->session->sessionID, 0xA5, ID_LEN); + } + + ExpectIntEQ(TLSX_Parse(ssl, extBytes, (word16)sizeof(extBytes), + server_hello, NULL), 0); + ExpectIntEQ(test_custom_ext_parse_called, 0); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +/* When resumption was attempted but the server falls back to a full handshake + * (session ID not echoed), custom extensions must still be validated: an + * unsolicited type is rejected, not silently ignored. */ +int test_wolfSSL_custom_ext_resumption_fallback(void) +{ + EXPECT_DECLS; +#if defined(OPENSSL_EXTRA) && defined(HAVE_TLS_EXTENSIONS) && \ + !defined(NO_TLS) && !defined(NO_WOLFSSL_CLIENT) && !defined(WOLFSSL_NO_TLS12) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + const byte extBytes[] = { 0x42, 0x42, 0x00, 0x04, 'w', 'o', 'l', 'f' }; + + test_custom_ext_reset(); + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + ExpectIntEQ(wolfSSL_CTX_add_client_custom_ext(ctx, TEST_CUSTOM_EXT_TYPE, + test_custom_ext_add_cb, test_custom_ext_free_cb, TEST_CUSTOM_ADD_ARG, + test_custom_ext_parse_cb, TEST_CUSTOM_PARSE_ARG), WOLFSSL_SUCCESS); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + + /* Resumption attempted, but no matching session ID => full handshake. The + * extension was never sent (no BuildRequest), so it is unsolicited and must + * be rejected rather than ignored on the optimistic resuming flag. */ + if (ssl != NULL) { + ssl->options.resuming = 1; + ssl->options.haveSessionId = 0; + } + + ExpectIntEQ(TLSX_Parse(ssl, extBytes, (word16)sizeof(extBytes), + server_hello, NULL), + WC_NO_ERR_TRACE(UNSUPPORTED_EXTENSION)); + ExpectIntEQ(test_custom_ext_parse_called, 0); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +/* A cached session ticket present during a resumption attempt must NOT by + * itself suppress custom-extension handling: if the server does not echo our + * session ID (it fell back to a full handshake or will issue a new ticket), the + * extension is still validated, so an unsolicited one is rejected. */ +int test_wolfSSL_custom_ext_ticket_fallback(void) +{ + EXPECT_DECLS; +#if defined(OPENSSL_EXTRA) && defined(HAVE_TLS_EXTENSIONS) && \ + !defined(NO_TLS) && !defined(NO_WOLFSSL_CLIENT) && \ + !defined(WOLFSSL_NO_TLS12) && defined(HAVE_SESSION_TICKET) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + const byte extBytes[] = { 0x42, 0x42, 0x00, 0x04, 'w', 'o', 'l', 'f' }; + + test_custom_ext_reset(); + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + ExpectIntEQ(wolfSSL_CTX_add_client_custom_ext(ctx, TEST_CUSTOM_EXT_TYPE, + test_custom_ext_add_cb, test_custom_ext_free_cb, TEST_CUSTOM_ADD_ARG, + test_custom_ext_parse_cb, TEST_CUSTOM_PARSE_ARG), WOLFSSL_SUCCESS); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + + /* A ticket is cached and resumption is attempted, but the server did not + * echo our session ID. ticketLenAlloc stays 0 so no buffer is freed. */ + if (ssl != NULL && ssl->session != NULL) { + ssl->options.resuming = 1; + ssl->options.haveSessionId = 0; + ssl->session->ticketLen = 16; + } + + ExpectIntEQ(TLSX_Parse(ssl, extBytes, (word16)sizeof(extBytes), + server_hello, NULL), + WC_NO_ERR_TRACE(UNSUPPORTED_EXTENSION)); + ExpectIntEQ(test_custom_ext_parse_called, 0); + + /* Avoid the test teardown treating the bogus length as a real ticket. */ + if (ssl != NULL && ssl->session != NULL) + ssl->session->ticketLen = 0; + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + /* use_srtp is only valid in ClientHello/ServerHello (pre-TLS 1.3) or * ClientHello/EncryptedExtensions (TLS 1.3) per RFC 5764. Feeding it in a * Finished message must be rejected with EXT_NOT_ALLOWED. */ diff --git a/tests/api/test_tls_ext.h b/tests/api/test_tls_ext.h index 90ccdf17c96..429216498e3 100644 --- a/tests/api/test_tls_ext.h +++ b/tests/api/test_tls_ext.h @@ -42,5 +42,16 @@ int test_TLSX_SRTP_msg_type_validation(void); int test_TLSX_ALPN_server_response_count(void); int test_TLSX_SupportedCurve_empty_or_unsupported(void); int test_TLSX_PointFormat_uncompressed_required(void); +int test_wolfSSL_CTX_add_client_custom_ext(void); +int test_wolfSSL_custom_ext_handshake(void); +int test_wolfSSL_custom_ext_flexible_handshake(void); +int test_wolfSSL_custom_ext_tls13_handshake(void); +int test_wolfSSL_custom_ext_parse(void); +int test_wolfSSL_custom_ext_unsolicited(void); +int test_wolfSSL_custom_ext_duplicate(void); +int test_wolfSSL_custom_ext_resumption_ignored(void); +int test_wolfSSL_custom_ext_resumption_fallback(void); +int test_wolfSSL_custom_ext_ticket_fallback(void); +int test_wolfSSL_custom_ext_add_null(void); -#endif /* TESTS_API_TEST_TLS_EMS_H */ +#endif /* TESTS_API_TEST_TLS_EXT_H */ diff --git a/wolfssl/internal.h b/wolfssl/internal.h index 8be44884f71..8e3b762520b 100644 --- a/wolfssl/internal.h +++ b/wolfssl/internal.h @@ -3215,6 +3215,31 @@ struct TLSX { struct TLSX* next; /* List Behavior */ }; +#if defined(HAVE_TLS_EXTENSIONS) && defined(OPENSSL_EXTRA) +/* OpenSSL-compatible custom (application-defined) TLS extension. + * Registered on a WOLFSSL_CTX via wolfSSL_CTX_add_client_custom_ext(). These + * extensions are not part of the TLSX framework but are processed in parallel + * for unknown extension types. Currently the client side for TLS 1.2 and below + * is supported, mirroring SSL_CTX_add_client_custom_ext(). */ +typedef struct WOLFSSL_CustomExt { + word16 ext_type; /* extension type on the wire */ + wolfSSL_custom_ext_add_cb add_cb; /* build outgoing extension data */ + wolfSSL_custom_ext_free_cb free_cb; /* free data produced by add_cb */ + wolfSSL_custom_ext_parse_cb parse_cb; /* parse incoming extension data */ + void* add_arg; /* opaque arg for add_cb/free_cb */ + void* parse_arg; /* opaque arg for parse_cb */ + struct WOLFSSL_CustomExt* next; /* list behaviour */ +} WOLFSSL_CustomExt; + +WOLFSSL_LOCAL void TLSX_CustomExt_FreeAll(WOLFSSL_CustomExt* list, void* heap); +#ifdef WOLFSSL_API_PREFIX_MAP + #define TLSX_CustomExt_BuildRequest wolfSSL_TLSX_CustomExt_BuildRequest +#endif +WOLFSSL_TEST_VIS int TLSX_CustomExt_BuildRequest(WOLFSSL* ssl, word16* pSz); +WOLFSSL_LOCAL int TLSX_CustomExt_Parse(WOLFSSL* ssl, byte msgType, word16 type, + const byte* input, word16 size, int* found); +#endif /* HAVE_TLS_EXTENSIONS && OPENSSL_EXTRA */ + #ifdef WOLFSSL_API_PREFIX_MAP #define TLSX_Find wolfSSL_TLSX_Find #endif @@ -4220,6 +4245,9 @@ struct WOLFSSL_CTX { int devId; /* async device id to use */ #ifdef HAVE_TLS_EXTENSIONS TLSX* extensions; /* RFC 6066 TLS Extensions data */ + #ifdef OPENSSL_EXTRA + WOLFSSL_CustomExt* customExt; /* App-defined custom TLS extensions */ + #endif #ifndef NO_WOLFSSL_SERVER #if defined(HAVE_CERTIFICATE_STATUS_REQUEST) \ || defined(HAVE_CERTIFICATE_STATUS_REQUEST_V2) @@ -6434,6 +6462,18 @@ struct WOLFSSL { #endif #ifdef HAVE_TLS_EXTENSIONS TLSX* extensions; /* RFC 6066 TLS Extensions data */ + #ifdef OPENSSL_EXTRA + /* Pre-built wire bytes for app-defined custom extensions in the + * ClientHello. Produced in TLSX_GetRequestSize and consumed (then + * freed) in TLSX_WriteRequest. See WOLFSSL_CustomExt. */ + byte* customExtData; + word16 customExtSz; + /* Custom extension types actually emitted in the ClientHello, so an + * unsolicited type echoed by the server can be rejected. Rebuilt with + * customExtData; persists until the connection is freed. */ + word16* customExtSent; + word16 customExtSentCnt; + #endif #ifdef HAVE_MAX_FRAGMENT word16 max_fragment; #endif diff --git a/wolfssl/openssl/ssl.h b/wolfssl/openssl/ssl.h index cdc8ea64adb..ae48c7a5f33 100644 --- a/wolfssl/openssl/ssl.h +++ b/wolfssl/openssl/ssl.h @@ -1123,6 +1123,14 @@ wolfSSL_X509_STORE_set_verify_cb((WOLFSSL_X509_STORE *)(s), (WOLFSSL_X509_STORE_ #define SSL_set_info_callback wolfSSL_set_info_callback #define SSL_CTX_set_alpn_protos wolfSSL_CTX_set_alpn_protos +/* Application-defined ("custom") TLS extensions. */ +#if defined(OPENSSL_EXTRA) && defined(HAVE_TLS_EXTENSIONS) +typedef wolfSSL_custom_ext_add_cb custom_ext_add_cb; +typedef wolfSSL_custom_ext_free_cb custom_ext_free_cb; +typedef wolfSSL_custom_ext_parse_cb custom_ext_parse_cb; +#define SSL_CTX_add_client_custom_ext wolfSSL_CTX_add_client_custom_ext +#endif /* OPENSSL_EXTRA && HAVE_TLS_EXTENSIONS */ + #define SSL_CTX_keylog_cb_func wolfSSL_CTX_keylog_cb_func #define SSL_CTX_set_keylog_callback wolfSSL_CTX_set_keylog_callback #define SSL_CTX_get_keylog_callback wolfSSL_CTX_get_keylog_callback diff --git a/wolfssl/ssl.h b/wolfssl/ssl.h index 21377bc28f1..f3fc7ed06b0 100644 --- a/wolfssl/ssl.h +++ b/wolfssl/ssl.h @@ -5055,6 +5055,34 @@ WOLFSSL_API int wolfSSL_CTX_set_num_tickets(WOLFSSL_CTX* ctx, size_t mxTickets); #endif /* HAVE_SESSION_TICKET */ +#if defined(OPENSSL_EXTRA) && defined(HAVE_TLS_EXTENSIONS) +/* OpenSSL-compatible application-defined ("custom") TLS extension callbacks. + * + * add_cb is invoked while building an outgoing message. On return: + * 1 - include the extension; out and outlen describe the data to send. + * 0 - omit the extension. + * <0 - fatal error; *al holds the TLS alert to send. + * If add_cb is NULL a zero-length extension is added to the ClientHello. + * + * free_cb (if set) is called after the data returned by add_cb has been + * copied into the message, to release any allocation it made. + * + * parse_cb is invoked for a received extension of the registered type. On + * return 1 the handshake continues; on return <=0 it is aborted with the + * alert placed in *al. */ +typedef int (*wolfSSL_custom_ext_add_cb)(WOLFSSL* s, unsigned int ext_type, + const unsigned char** out, size_t* outlen, int* al, void* add_arg); +typedef void (*wolfSSL_custom_ext_free_cb)(WOLFSSL* s, unsigned int ext_type, + const unsigned char* out, void* add_arg); +typedef int (*wolfSSL_custom_ext_parse_cb)(WOLFSSL* s, unsigned int ext_type, + const unsigned char* in, size_t inlen, int* al, void* parse_arg); + +WOLFSSL_API int wolfSSL_CTX_add_client_custom_ext(WOLFSSL_CTX* ctx, + unsigned int ext_type, wolfSSL_custom_ext_add_cb add_cb, + wolfSSL_custom_ext_free_cb free_cb, void* add_arg, + wolfSSL_custom_ext_parse_cb parse_cb, void* parse_arg); +#endif /* OPENSSL_EXTRA && HAVE_TLS_EXTENSIONS */ + /* TLS Extended Master Secret Extension */ WOLFSSL_API int wolfSSL_DisableExtendedMasterSecret(WOLFSSL* ssl); WOLFSSL_API int wolfSSL_CTX_DisableExtendedMasterSecret(WOLFSSL_CTX* ctx); From 74605e501b59029d2427c38a00558eee4a55cdae Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Wed, 10 Jun 2026 23:08:12 +0000 Subject: [PATCH 2/4] Reword comments so 'custom' is not picked up as a macro by check-source-text The check-source-text analyzer extracts macro-like tokens from 'defined (token)' sequences anywhere in tracked files, including prose in comments. The phrase 'application-defined (custom) extensions' made it report: unrecognized macros used: custom. Reorder the wording to 'custom (application-defined)' to keep the analyzer happy. --- src/tls.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tls.c b/src/tls.c index d6d0b46ac89..f5a8e66edd0 100644 --- a/src/tls.c +++ b/src/tls.c @@ -17423,7 +17423,7 @@ int TLSX_GetRequestSize(WOLFSSL* ssl, byte msgType, word32* pLength) #endif #if defined(HAVE_TLS_EXTENSIONS) && defined(OPENSSL_EXTRA) - /* Application-defined (custom) extensions. These are always offered in the + /* Custom (application-defined) extensions. These are always offered in the * ClientHello regardless of the client's maximum version (matching OpenSSL, * whose is_tls13 check is false while constructing the ClientHello), so * they work with flexible client methods that go on to negotiate TLS 1.2. @@ -17670,7 +17670,7 @@ int TLSX_WriteRequest(WOLFSSL* ssl, byte* output, byte msgType, word32* pOffset) #endif #if defined(HAVE_TLS_EXTENSIONS) && defined(OPENSSL_EXTRA) - /* Copy out the application-defined (custom) extension bytes built during + /* Copy out the custom (application-defined) extension bytes built during * TLSX_GetRequestSize, then release the cached buffer. */ if (msgType == client_hello && ssl->customExtData != NULL) { WOLFSSL_MSG("Custom extensions to write"); @@ -19009,7 +19009,7 @@ WOLFSSL_TEST_VIS int TLSX_Parse(WOLFSSL* ssl, const byte* input, word16 length, default: #if defined(HAVE_TLS_EXTENSIONS) && defined(OPENSSL_EXTRA) { - /* Application-defined (custom) extension handler, if one + /* Custom (application-defined) extension handler, if one * was registered for this type. */ int customFound = 0; ret = TLSX_CustomExt_Parse(ssl, msgType, type, From 18aef5a43b68891bde2fa83a77e0c978adff1fb4 Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Thu, 11 Jun 2026 11:41:49 +0000 Subject: [PATCH 3/4] google.test: skip when the TCP connection cannot be opened PRB nodes intermittently fail google.test with 'tcp connect failed: Connection timed out' while www.google.com still answers ping and www.wolfssl.com:443 (external.test) connects fine: Google drops or throttles TCP connections from busy CI egress IPs, so the existing ping reachability guard does not catch it. Failing to even open the TCP connection exercises no wolfSSL code, so treat it like the unreachable-server case and skip (77) instead of failing. TLS-level failures still fail the test. --- scripts/google.test | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/scripts/google.test b/scripts/google.test index 5e3e8f0f96f..bedd7e289db 100755 --- a/scripts/google.test +++ b/scripts/google.test @@ -25,14 +25,30 @@ fi RESULT=$? [ $RESULT -ne 0 ] && exit 0 +# Run the client against the server. The server may answer ping but still drop +# or throttle TCP connections from this host (Google does this to busy CI +# egress IPs). Failing to open the TCP connection exercises no wolfSSL code, +# so treat it like the unreachable-server case above and skip instead of +# failing. +run_client() { + OUTPUT="$(./examples/client/client "$@" 2>&1)" + RESULT=$? + echo "$OUTPUT" + if [ $RESULT -ne 0 ] && echo "$OUTPUT" | grep -q 'tcp connect failed'; then + echo -e "\n\ntcp connect to $server failed, skipping" + exit 77 + fi + return $RESULT +} + # client test against the server -./examples/client/client -X -C -h $server -p 443 -g -d +run_client -X -C -h $server -p 443 -g -d RESULT=$? [ $RESULT -ne 0 ] && echo -e "\n\nClient connection failed" && exit 1 if ./examples/client/client -V | grep -q 4; then # client test against the server using TLS v1.3 - ./examples/client/client -v 4 -X -C -h $server -p 443 -g -d + run_client -v 4 -X -C -h $server -p 443 -g -d RESULT=$? [ $RESULT -ne 0 ] && echo -e "\n\nTLSv1.3 Client connection failed" && exit 1 fi From c5faca16f14e8ff69a64f03298db4f04288e0394 Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Mon, 13 Jul 2026 19:00:41 +0000 Subject: [PATCH 4/4] Address review: extract per-extension custom ext build into a helper Move the ClientHello custom-extension add/serialize loop body of TLSX_CustomExt_BuildRequest into TLSX_CustomExt_AddOne. Behavior is unchanged; the loop now delegates each extension to the helper. --- src/tls.c | 153 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 85 insertions(+), 68 deletions(-) diff --git a/src/tls.c b/src/tls.c index f5a8e66edd0..483c7d47a2f 100644 --- a/src/tls.c +++ b/src/tls.c @@ -17105,6 +17105,90 @@ void TLSX_CustomExt_FreeAll(WOLFSSL_CustomExt* list, void* heap) } } +/* Builds one custom extension via its add callback and appends it (type, + * length, data) to the buffer tracked by *pData / *pDataSz, recording the type + * in ssl->customExtSent. Runs the matching free callback for the add. Returns 0 + * when the extension was appended or intentionally omitted, otherwise a + * negative error. */ +static int TLSX_CustomExt_AddOne(WOLFSSL* ssl, WOLFSSL_CustomExt* meth, + byte** pData, word32* pDataSz) +{ + const unsigned char* out = NULL; + size_t outlen = 0; + int al = unsupported_extension; + int addRet = 1; /* no add_cb => add a zero-length extension */ + word32 need = 0; + byte* tmp = NULL; + word16* sent = NULL; + byte* data = *pData; + word32 dataSz = *pDataSz; + int ret = 0; + + if (meth->add_cb != NULL) { + addRet = meth->add_cb(ssl, meth->ext_type, &out, &outlen, &al, + meth->add_arg); + } + + if (addRet < 0) { + /* Fatal: callback requested the connection be aborted. add_cb + * returned < 0, so free_cb is not run (skips free_ext). */ + SendAlert(ssl, alert_fatal, (byte)al); + return WOLFSSL_FATAL_ERROR; + } + if (addRet == 0) + return 0; /* extension omitted for this message */ + + if (out == NULL && outlen > 0) { + ret = BAD_FUNC_ARG; + } + else if (outlen > WOLFSSL_MAX_16BIT) { + ret = BUFFER_ERROR; + } + else { + need = HELLO_EXT_TYPE_SZ + OPAQUE16_LEN + (word32)outlen; + if (dataSz + need > (word32)WOLFSSL_MAX_16BIT) + ret = BUFFER_ERROR; + } + if (ret != 0) + goto free_ext; + + tmp = (byte*)XREALLOC(data, dataSz + need, ssl->heap, + DYNAMIC_TYPE_TMP_BUFFER); + if (tmp == NULL) { + ret = MEMORY_E; + goto free_ext; + } + data = tmp; + + c16toa(meth->ext_type, data + dataSz); + dataSz += HELLO_EXT_TYPE_SZ; + c16toa((word16)outlen, data + dataSz); + dataSz += OPAQUE16_LEN; + if (outlen > 0) { + XMEMCPY(data + dataSz, out, outlen); + dataSz += (word32)outlen; + } + + /* Record the type as sent so the server may legitimately echo it. */ + sent = (word16*)XREALLOC(ssl->customExtSent, + (ssl->customExtSentCnt + 1) * (word32)sizeof(word16), + ssl->heap, DYNAMIC_TYPE_TLSX); + if (sent == NULL) { + ret = MEMORY_E; + goto free_ext; + } + ssl->customExtSent = sent; + ssl->customExtSent[ssl->customExtSentCnt++] = meth->ext_type; + +free_ext: + if (meth->free_cb != NULL) + meth->free_cb(ssl, meth->ext_type, out, meth->add_arg); + + *pData = data; + *pDataSz = dataSz; + return ret; +} + /* Invokes the registered add callbacks and serializes the resulting custom * extensions for the ClientHello into ssl->customExtData. The total wire size * (type + length + data for each included extension) is returned in *pSz. The @@ -17133,74 +17217,7 @@ WOLFSSL_TEST_VIS int TLSX_CustomExt_BuildRequest(WOLFSSL* ssl, word16* pSz) for (meth = ssl->ctx->customExt; meth != NULL && ret == 0; meth = meth->next) { - const unsigned char* out = NULL; - size_t outlen = 0; - int al = unsupported_extension; - int addRet = 1; /* no add_cb => add a zero-length extension */ - word32 need = 0; - byte* tmp = NULL; - word16* sent = NULL; - - if (meth->add_cb != NULL) { - addRet = meth->add_cb(ssl, meth->ext_type, &out, &outlen, &al, - meth->add_arg); - } - - if (addRet < 0) { - /* Fatal: callback requested the connection be aborted. add_cb - * returned < 0, so free_cb is not run (skips free_ext). */ - SendAlert(ssl, alert_fatal, (byte)al); - ret = WOLFSSL_FATAL_ERROR; - break; - } - if (addRet == 0) - continue; /* extension omitted for this message */ - - if (out == NULL && outlen > 0) { - ret = BAD_FUNC_ARG; - } - else if (outlen > WOLFSSL_MAX_16BIT) { - ret = BUFFER_ERROR; - } - else { - need = HELLO_EXT_TYPE_SZ + OPAQUE16_LEN + (word32)outlen; - if (dataSz + need > (word32)WOLFSSL_MAX_16BIT) - ret = BUFFER_ERROR; - } - if (ret != 0) - goto free_ext; - - tmp = (byte*)XREALLOC(data, dataSz + need, ssl->heap, - DYNAMIC_TYPE_TMP_BUFFER); - if (tmp == NULL) { - ret = MEMORY_E; - goto free_ext; - } - data = tmp; - - c16toa(meth->ext_type, data + dataSz); - dataSz += HELLO_EXT_TYPE_SZ; - c16toa((word16)outlen, data + dataSz); - dataSz += OPAQUE16_LEN; - if (outlen > 0) { - XMEMCPY(data + dataSz, out, outlen); - dataSz += (word32)outlen; - } - - /* Record the type as sent so the server may legitimately echo it. */ - sent = (word16*)XREALLOC(ssl->customExtSent, - (ssl->customExtSentCnt + 1) * (word32)sizeof(word16), - ssl->heap, DYNAMIC_TYPE_TLSX); - if (sent == NULL) { - ret = MEMORY_E; - goto free_ext; - } - ssl->customExtSent = sent; - ssl->customExtSent[ssl->customExtSentCnt++] = meth->ext_type; - -free_ext: - if (meth->free_cb != NULL) - meth->free_cb(ssl, meth->ext_type, out, meth->add_arg); + ret = TLSX_CustomExt_AddOne(ssl, meth, &data, &dataSz); } if (ret != 0) {