Skip to content

Add SSL_CTX_add_client_custom_ext (OpenSSL-compat client custom extensions)#10625

Draft
julek-wolfssl wants to merge 6 commits into
wolfSSL:masterfrom
julek-wolfssl:client-custom-ext
Draft

Add SSL_CTX_add_client_custom_ext (OpenSSL-compat client custom extensions)#10625
julek-wolfssl wants to merge 6 commits into
wolfSSL:masterfrom
julek-wolfssl:client-custom-ext

Conversation

@julek-wolfssl
Copy link
Copy Markdown
Member

Implements the OpenSSL-compatible legacy client custom extension API
(SSL_CTX_add_client_custom_ext) for TLS 1.2 and below.

Application-defined extensions carry arbitrary IANA types, which cannot live
in the TLSX list (it keys every extension on a fixed 72-bit 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,
mirroring OpenSSL's custext.

Commits

  1. Add SSL_CTX_add_client_custom_ext — registers a {add,free,parse}
    method set with validation. ClientHello runs each add_cb and serializes
    the wire bytes; ServerHello dispatches the unknown-type case to parse_cb.
    Gated to non-TLS-1.3 so TLS 1.3 keeps RFC 8446 unsupported_extension
    behavior. Lists/buffers freed in SSL_CtxResourceFree/SSL_ResourceFree;
    openssl/ssl.h maps the API and custom_ext_*_cb typedefs.

  2. Reject unsolicited/duplicate exts and ignore on resumption — tracks the
    custom types emitted in the ClientHello and rejects a server echo of an
    unsent type (unsupported_extension), suppresses parsing on a resumed
    handshake (SSL_EXT_IGNORE_ON_RESUMPTION), and scans for duplicate custom
    types (DUPLICATE_TLS_EXT_E), matching extensions_cust.c/extensions.c.

  3. Confirmed-resumption gate and flexible client methods — gates the
    resumption ignore on the server-confirmed signal (resumption attempted AND
    the server echoed our session ID), and always offers the extension in the
    ClientHello regardless of the method's max version, so
    wolfSSLv23_client_method()/wolfTLS_client_method() offer it when
    negotiating TLS 1.2. The negotiated-version restriction remains enforced on
    the parse side.

Adds unit tests covering registration validation, TLS 1.2/1.3 and
flexible-method handshakes (add/free callbacks balance), ServerHello parse
dispatch, and unsolicited/duplicate/resumption handling. Builds with and
without OPENSSL_EXTRA; full API suite passes.

…sions)

Implements the OpenSSL-compatible legacy client custom extension API for
TLS 1.2 and below. Application-defined extensions carry arbitrary IANA
types, which cannot live in the TLSX list (it keys every extension on a
fixed 72-bit 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, mirroring OpenSSL's custext.

- wolfSSL_CTX_add_client_custom_ext() registers a {add,free,parse} method
  set with validation (ext_type <= 0xffff, no free_cb without add_cb, not
  an internally handled type, no duplicates).
- ClientHello: 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).
- ServerHello: the unknown-type case in TLSX_Parse dispatches to parse_cb
  (<=0 aborts with the requested alert). Gated to non-TLS-1.3 so TLS 1.3
  keeps RFC 8446 unsupported_extension behavior.
- Lists/buffers freed in SSL_CtxResourceFree and SSL_ResourceFree.
- openssl/ssl.h maps SSL_CTX_add_client_custom_ext and the custom_ext_*_cb
  typedefs.

Adds unit tests covering registration validation, a TLS 1.2 memio
handshake (add/free callbacks balance) and ServerHello parse dispatch.
Builds with and without OPENSSL_EXTRA; full API suite passes.
Address three OpenSSL-compatibility gaps in the client custom extension
handling, matching ssl/statem/extensions_cust.c and extensions.c:

1. Unsolicited responses: track the custom extension types actually
   emitted in the ClientHello (ssl->customExtSent, built alongside
   customExtData). On ServerHello, a custom extension whose type was not
   sent is rejected with unsupported_extension, mirroring OpenSSL's
   SSL_EXT_FLAG_SENT check. add_cb declining to send a type for a given
   handshake now correctly makes a server echo unsolicited.

2. Resumption: OpenSSL registers the legacy API with
   SSL_EXT_IGNORE_ON_RESUMPTION and skips parsing on a resumed handshake.
   Suppress custom-extension parsing when ssl->options.resuming is set so
   a server echo is silently ignored. (Evaluated at ServerHello-parse
   time, i.e. the optimistic resumption-attempt state, since wolfSSL
   finalizes resumption later in CompleteServerHello. The send path is
   left intact, matching OpenSSL where s->hit is 0 at ClientHello.)

3. Duplicates: the semaphore-based duplicate detection cannot cover
   arbitrary custom types. Scan the already-parsed portion of the message
   for an earlier extension of the same registered custom type and abort
   with DUPLICATE_TLS_EXT_E, matching OpenSSL giving each custom extension
   its own slot.

Adds tests for unsolicited rejection, duplicate rejection, and
resumption ignore; updates the parse test to emit first. Builds with and
without OPENSSL_EXTRA; full API suite passes.
…hods

Follow-up addressing two issues from review of the previous fix:

1. Resumption (was: parse skipped on optimistic resuming flag, then on a
   cached-ticket heuristic). At ServerHello-parse time ssl->options.resuming
   only means the client *attempted* resumption, and a cached session ticket
   does not mean the server agreed to resume. Gate the ignore on the actual
   server-confirmed signal: resumption was attempted AND the server echoed our
   session ID back (RFC 5246, and RFC 5077 for tickets with a non-empty session
   ID). A server that falls back to a full handshake -- whether plain or after
   a ticket attempt -- does not echo our session ID, so its extensions are
   still parsed/validated and an unsolicited one is rejected.

2. Flexible client methods (was: API disabled when max version is TLS 1.3).
   The send path gated on ssl->version < TLS 1.3, but before the handshake that
   field holds the method's max version, so wolfSSLv23_client_method() /
   wolfTLS_client_method() (initialized to TLS 1.3) never offered the extension
   even when negotiating TLS 1.2. The ClientHello now always offers the
   extension regardless of max version, matching OpenSSL (is_tls13 is false
   while building the ClientHello). The negotiated-version restriction remains
   enforced on the parse side, where ssl->version is the negotiated version
   (TLS 1.3 ServerHellos are routed to DoTls13ServerHello before this code).

Adds tests: flexible-method (v23 client -> TLS 1.2) and TLS 1.3 handshakes both
offer the extension without breaking; resumption-fallback and ticket-fallback
reject an unsolicited extension; resumption-ignored sets up a matching session
ID. Builds with and without OPENSSL_EXTRA; full API suite passes.
Copilot AI review requested due to automatic review settings June 5, 2026 18:04
@julek-wolfssl julek-wolfssl self-assigned this Jun 5, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Implements OpenSSL-compatible legacy client custom extensions (SSL_CTX_add_client_custom_ext) for TLS 1.2 and below, including registration, ClientHello serialization, ServerHello parse dispatch, and related validation behaviors.

Changes:

  • Added public API + OpenSSL-compat typedef/macro mappings for client custom extensions.
  • Introduced internal storage and processing for app-defined custom extensions alongside unknown-extension handling.
  • Added unit tests for registration validation, handshake behavior across TLS 1.2/1.3, parse dispatch, and unsolicited/duplicate/resumption handling.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
wolfssl/ssl.h Adds public callback typedefs and wolfSSL_CTX_add_client_custom_ext API declaration.
wolfssl/openssl/ssl.h Maps OpenSSL names (custom_ext_*_cb, SSL_CTX_add_client_custom_ext) onto wolfSSL API.
wolfssl/internal.h Introduces WOLFSSL_CustomExt and adds per-CTX/per-SSL fields for custom ext wire bytes and sent-type tracking.
src/tls.c Implements custom ext registration, ClientHello build/write integration, parse dispatch, and duplicate detection.
src/internal.c Frees custom ext lists/buffers during ctx/ssl teardown.
tests/api/test_tls_ext.h Exposes new custom-ext test declarations.
tests/api/test_tls_ext.c Adds comprehensive unit tests for custom-ext behaviors.
tests/api.c Registers new test cases in the test runner.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread wolfssl/ssl.h Outdated
Comment thread src/tls.c
Comment thread src/tls.c Outdated
Comment thread tests/api/test_tls_ext.c Outdated
Comment thread tests/api/test_tls_ext.h Outdated
The unit tests in tests/api/test_tls_ext.c call TLSX_CustomExt_BuildRequest
directly, but it was declared WOLFSSL_LOCAL, so it is given hidden visibility
and is not exported from libwolfssl.so. Linking tests/unit.test against the
shared library therefore failed with 'undefined reference to
TLSX_CustomExt_BuildRequest' in every opensslextra-enabled smoke build.

Declare it WOLFSSL_TEST_VIS (matching TLSX_Find, TLSX_Parse, etc.) so the
symbol is exported when test visibility is enabled and remains WOLFSSL_LOCAL
in production builds. Also add the WOLFSSL_API_PREFIX_MAP rename to match the
surrounding convention.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Comment thread src/tls.c
Comment thread src/tls.c
- Gate the public custom-ext typedefs/declaration (wolfssl/ssl.h) and the
  OpenSSL-compat typedefs/macro (wolfssl/openssl/ssl.h) on HAVE_TLS_EXTENSIONS
  in addition to OPENSSL_EXTRA, matching the implementation guard so an
  OPENSSL_EXTRA-only build can no longer expose an unbuildable API.
- Replace the hard-coded 62 semaphore boundary in TLSX_ToSemaphore() and
  TLSX_Parse() with a documented SEMAPHORE_MAX_DIRECT_TYPE constant so the
  duplicate-detection boundary lives in one place.
- Mark the TLSX_CustomExt_BuildRequest definition WOLFSSL_TEST_VIS to match its
  internal.h declaration (the codebase repeats WOLFSSL_TEST_VIS on definitions
  but not WOLFSSL_LOCAL, which is inherited from the header).
- Use the addresses of static objects for the custom-ext test add/parse args
  instead of casting integer constants to void*.
- Fix the test_tls_ext.h include-guard #endif comment.
Address the second round of review on wolfSSL#10625:

- TLSX_CustomExt_BuildRequest: if an add_cb returns 1 with outlen > 0 but
  leaves *out == NULL, reject with BAD_FUNC_ARG (running free_cb for cleanup)
  instead of dereferencing NULL in the XMEMCPY.
- TLSX_GetRequestSize: the extensions accumulator is a word16, so a large
  custom extension could wrap "length += customSz" before the block-length
  check and under-report the buffer size, risking an out-of-bounds write in
  TLSX_WriteRequest. Reject an oversized total using a wider type before the
  addition.

Adds test_wolfSSL_custom_ext_add_null covering the NULL-pointer path (clean
failure plus free_cb invocation).
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Comment thread src/tls.c
Comment on lines +16809 to +16816
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;

*pSz = 0;
Comment thread src/tls.c
Comment on lines +16984 to +16990
if (ssl->options.resuming && ssl->options.haveSessionId &&
ssl->arrays != NULL && ssl->session != NULL &&
ssl->arrays->sessionIDSz == ID_LEN &&
ssl->session->sessionIDSz == ID_LEN &&
XMEMCMP(ssl->arrays->sessionID, ssl->session->sessionID,
ID_LEN) == 0) {
return 0;
Comment thread src/tls.c
Comment on lines +18036 to +18047
else if (type > SEMAPHORE_MAX_DIRECT_TYPE &&
TLSX_CustomExt_IsRegistered(ssl, type)) {
word16 scan = 0;
word16 upto = (word16)(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 = (word16)(scan + HELLO_EXT_TYPE_SZ + OPAQUE16_LEN + sS);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants