Skip to content

Add WOLFSSL_STRICT_CIPHER_LIST for TLS 1.3-only cipher lists on a non-TLS1.3 ctx/ssl - #10963

Open
miyazakh wants to merge 5 commits into
wolfSSL:masterfrom
miyazakh:f3228_cipher_list
Open

Add WOLFSSL_STRICT_CIPHER_LIST for TLS 1.3-only cipher lists on a non-TLS1.3 ctx/ssl#10963
miyazakh wants to merge 5 commits into
wolfSSL:masterfrom
miyazakh:f3228_cipher_list

Conversation

@miyazakh

Copy link
Copy Markdown
Contributor

Summary(f3228)

  • wolfSSL_CTX_set_cipher_list() / wolfSSL_set_cipher_list() silently
    return WOLFSSL_SUCCESS without touching the configured suites when
    given a cipher list that names only TLS 1.3 suites, but the ctx/ssl
    cannot negotiate TLS 1.3 (OPENSSL_EXTRA builds only, via
    wolfSSL_parse_cipher_list() in src/ssl.c).
  • This mirrors OpenSSL's SSL_CTX_set_ciphersuites() compatibility shim,
    but a caller that treats the non-failing return as confirmation that
    the restriction took effect will keep negotiating whatever <= TLS 1.2
    suites were already configured (default or otherwise).
  • Add an opt-in WOLFSSL_STRICT_CIPHER_LIST build macro. When defined,
    the function returns WOLFSSL_FAILURE instead of silently ignoring
    the list. Off by default, so existing OpenSSL-compat behavior is
    unchanged unless a user explicitly opts in.

Changes

  • src/ssl.c: gate the return value in wolfSSL_parse_cipher_list()
    behind WOLFSSL_STRICT_CIPHER_LIST.
  • src/internal.c: document the new macro in the build-options header
    comment (Cipher Suite Selection section).
  • .wolfssl_known_macro_extras: register WOLFSSL_STRICT_CIPHER_LIST.
  • tests/api/test_tls13.c, tests/api/test_tls13.h: add
    test_tls13_cipher_list_no_tls13_ctx, covering both the default
    (WOLFSSL_SUCCESS) and strict (WOLFSSL_FAILURE) behavior.

Testing

  • ./configure --enable-opensslextra --enable-tls13 --enable-debug && make — builds clean.
  • ./tests/unit.test (default, WOLFSSL_STRICT_CIPHER_LIST
    undefined): Failed/Skipped/Passed/All: 0/314/1726/2040.
  • ./tests/unit.test (CPPFLAGS="-DWOLFSSL_STRICT_CIPHER_LIST"):
    Failed/Skipped/Passed/All: 0/314/1726/2040.
  • test_tls13_cipher_list_no_tls13_ctx passes in both
    configurations, exercising wolfSSL_CTX_set_cipher_list(ctx, "TLS13-AES128-GCM-SHA256") on a wolfTLSv1_2_client_method()
    context.

Checklist

  • added tests
  • updated/added doxygen
  • updated appropriate READMEs
  • Updated manual and documentation

@miyazakh miyazakh self-assigned this Jul 21, 2026
Copilot AI review requested due to automatic review settings July 21, 2026 22:36

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Adds an opt-in build macro to make wolfSSL_{CTX,}_set_cipher_list() fail (instead of silently succeeding) when a TLS 1.3-only cipher list is applied to a context/SSL that cannot negotiate TLS 1.3, and adds tests/documentation to cover the new behavior.

Changes:

  • Add WOLFSSL_STRICT_CIPHER_LIST to make TLS 1.3-only cipher lists return failure on non-TLS1.3 ctx/ssl.
  • Document and register the new macro in internal build-options docs and known-macro list.
  • Add a TLS 1.3 API test covering both default and strict behaviors.

Reviewed changes

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

Show a summary per file
File Description
src/ssl.c Gates return value to fail under WOLFSSL_STRICT_CIPHER_LIST for TLS 1.3-only lists on non-TLS1.3 ctx/ssl.
src/internal.c Documents the new build macro in the cipher suite selection options list.
.wolfssl_known_macro_extras Registers WOLFSSL_STRICT_CIPHER_LIST as a known macro.
tests/api/test_tls13.c Adds a new test exercising strict vs default behavior.
tests/api/test_tls13.h Declares and registers the new test in the TLS 1.3 test group.
Comments suppressed due to low confidence (1)

src/internal.c:1

  • The text wolfSSL_CTX/SSL_set_cipher_list() is ambiguous (it reads like a single function name). Consider updating to the exact API names (e.g., wolfSSL_CTX_set_cipher_list() / wolfSSL_set_cipher_list()) for clarity and consistency with the rest of the build-options documentation.

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

Comment thread src/ssl.c
Comment thread tests/api/test_tls13.c
@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown

MemBrowse Memory Report

gcc-arm-cortex-m3

  • FLASH: .text +72 B (+0.1%, 122,611 B / 262,144 B, total: 47% used)

gcc-arm-cortex-m4

  • FLASH: .text +128 B (+0.1%, 200,917 B / 262,144 B, total: 77% used)

gcc-arm-cortex-m4-dtls13

  • FLASH: .text +64 B (+0.0%, 181,092 B / 1,048,576 B, total: 17% used)

gcc-arm-cortex-m4-openssl-compat

  • FLASH: .rodata +104 B, .text -576 B (-0.1%, 771,412 B / 1,048,576 B, total: 74% used)
  • RAM: .bss +192 B (+0.1%, 136,936 B / 262,144 B, total: 52% used)

gcc-arm-cortex-m4-pkcs7

  • FLASH: .text +512 B (+0.2%, 213,908 B / 262,144 B, total: 82% used)

gcc-arm-cortex-m4-rsa-only

  • FLASH: .text +192 B (+0.1%, 326,104 B / 1,048,576 B, total: 31% used)

gcc-arm-cortex-m4-tls12

  • FLASH: .text +64 B (+0.1%, 123,355 B / 262,144 B, total: 47% used)

gcc-arm-cortex-m4-tls13

  • FLASH: .text -64 B (-0.0%, 236,615 B / 262,144 B, total: 90% used)

gcc-arm-cortex-m7

  • FLASH: .text +64 B (+0.0%, 200,853 B / 262,144 B, total: 77% used)

gcc-arm-cortex-m7-pq

  • FLASH: .text -64 B (-0.0%, 280,512 B / 1,048,576 B, total: 27% used)

gcc-arm-cortex-m7-tls13

  • FLASH: .text -64 B (-0.0%, 236,679 B / 262,144 B, total: 90% used)

linuxkm-pie

  • Data: __patchable_function_entries +296 B (+1.1%, 26,256 B)

linuxkm-standard

  • Data: __patchable_function_entries +392 B (+0.8%, 49,000 B)

stm32-sim-stm32h753

@miyazakh

Copy link
Copy Markdown
Contributor Author

retest this please

3 similar comments
@miyazakh

Copy link
Copy Markdown
Contributor Author

retest this please

@miyazakh

Copy link
Copy Markdown
Contributor Author

retest this please

@miyazakh

Copy link
Copy Markdown
Contributor Author

retest this please

@miyazakh miyazakh assigned wolfSSL-Bot and unassigned miyazakh Jul 24, 2026

@Frauschi Frauschi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🐺 Skoll Code Review

Overall recommendation: APPROVE
Findings: 3 total — 3 posted, 0 skipped

Posted findings

  • [Medium] Test only exercises the CTX path, not the SSL pathtests/api/test_tls13.c:2466-2489
  • [Low] Test asserts return value but not that suites are actually left untouchedtests/api/test_tls13.c:2480-2484
  • [Low] WOLFSSL_STRICT_CIPHER_LIST can now fail ctx creation under WOLFSSL_SYS_CRYPTO_POLICYsrc/ssl.c:636-641

Review generated by Skoll via Claude/Codex

Comment thread tests/api/test_tls13.c
Comment thread tests/api/test_tls13.c
Comment thread src/ssl.c
@Frauschi Frauschi assigned miyazakh and unassigned wolfSSL-Bot Jul 24, 2026
@miyazakh

Copy link
Copy Markdown
Contributor Author

Jenkins retest this please.

@Frauschi Frauschi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🐺 Skoll Code Review

Overall recommendation: APPROVE
Findings: 10 total — 4 posted, 6 skipped

Posted findings

  • [Medium] WOLFSSL_STRICT_CIPHER_LIST also breaks SSL_CTX_set_ciphersuites() OpenSSL semanticssrc/ssl.c:3898-3905
  • [Medium] WOLFSSL_STRICT_CIPHER_LIST guarantee is narrower than documented: version-cap via SSL_OP_NO_TLSv1_3 / set_max_proto_version is not detectedsrc/ssl.c:3891-3910
  • [Low] Strict branch of the new test is never compiled by any CI configurationtests/api/test_tls13.c:2481-2483
  • [Low] New regression test's "suites unchanged" assertions are vacuous (0 == 0) and never verify preservation of configured suitestests/api/test_tls13.c:2466-2523
Skipped findings
  • [Medium] Default-path test asserts 0 == 0; it does not prove prior suites are preserved
  • [Medium] New macro documented in src/internal.c although it is only used in src/ssl.c
  • [Low] Public API return contract changes but doxygen for set_cipher_list is not updated
  • [Low] Strict mode is not applied to the *_set_cipher_list_bytes() variants
  • [Low] Enabling WOLFSSL_STRICT_CIPHER_LIST turns wolfSSL_CTX_new() into a failing call under a TLS 1.3-only system crypto policy
  • [Info] Strict-mode rejection is indistinguishable from other set_cipher_list failures at the API boundary

Review generated by Skoll via Claude/Codex

Comment thread src/ssl.c
tls13Only = 1;
if ((ctx != NULL && !IsAtLeastTLSv1_3(ctx->method->version)) ||
(ssl != NULL && !IsAtLeastTLSv1_3(ssl->version))) {
#ifdef WOLFSSL_STRICT_CIPHER_LIST

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 [Medium] WOLFSSL_STRICT_CIPHER_LIST also breaks SSL_CTX_set_ciphersuites() OpenSSL semantics
💡 SUGGEST api

wolfssl/openssl/ssl.h:395 defines SSL_CTX_set_ciphersuites as wolfSSL_CTX_set_cipher_list, so the new strict return path applies to both compat APIs, not just set_cipher_list(). The comment three lines above the new code (src/ssl.c:3893) even says this branch exists to "simulate set_ciphersuites() compatibility layer API". In OpenSSL, SSL_CTX_set_ciphersuites() is deliberately version-independent: it configures the separate TLS 1.3 suite list and returns 1 for any syntactically valid suite name regardless of the ctx's max protocol version. Under a strict build the very common OpenSSL idiom now fails:

ctx = SSL_CTX_new(TLS_client_method());
SSL_CTX_set_max_proto_version(ctx, TLS1_2_VERSION);   /* lowers ctx->method->version.minor, see ssl.c:7463 */
SSL_CTX_set_ciphersuites(ctx, "TLS13-AES128-GCM-SHA256"); /* OpenSSL: 1, strict wolfSSL: 0 */

Set_CTX_max_proto_version() writes ctx->method->version.minor = TLSv1_2_MINOR (src/ssl.c:7463-7465), so IsAtLeastTLSv1_3(ctx->method->version) goes false and the new WOLFSSL_FAILURE triggers — even though the caller used the API whose whole purpose is setting 1.3 suites. Because both APIs funnel through the same function, the strict flag cannot distinguish "user called set_cipher_list" from "user called set_ciphersuites", so opting in unavoidably breaks the OpenSSL contract for the second one. It is opt-in and off by default, so nothing regresses for existing builds — but the doc block in src/internal.c only mentions set_cipher_list(), which understates the blast radius.

Suggestion:

Suggested change
#ifdef WOLFSSL_STRICT_CIPHER_LIST
* WOLFSSL_STRICT_CIPHER_LIST: Fail wolfSSL_CTX/SSL_set_cipher_list()
* when given a TLS 1.3-only suite list but TLS 1.3
* is not negotiable, instead of leaving the prior
* suites untouched and returning success. NOTE: since
* SSL_CTX_set_ciphersuites() maps to
* wolfSSL_CTX_set_cipher_list(), this also makes that
* API fail, which diverges from OpenSSL (there
* set_ciphersuites() succeeds regardless of the
* configured max protocol version). ...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

SSL_CTX/SSL_set_ciphersuites() alias to set_cipher_list(), so strict-mode failures apply to them too, but the doc only mentioned set_cipher_list(). Added a note in src/internal.c (~line 44) that set_ciphersuites() is also affected, including via SSL_CTX_set_max_proto_version().

Comment thread src/ssl.c
@@ -3895,8 +3895,17 @@ static int wolfSSL_parse_cipher_list(WOLFSSL_CTX* ctx, WOLFSSL* ssl,
tls13Only = 1;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 [Medium] WOLFSSL_STRICT_CIPHER_LIST guarantee is narrower than documented: version-cap via SSL_OP_NO_TLSv1_3 / set_max_proto_version is not detected
💡 SUGGEST

The new opt-in macro is documented (both in src/internal.c and in the added code comment) as failing wolfSSL_CTX_set_cipher_list() / wolfSSL_set_cipher_list() when "TLS 1.3 is not negotiable". The guard it hangs off, however, only inspects ctx->method->version and ssl->version. Those fields are NOT updated by two common OpenSSL-compat ways of capping a connection at TLS 1.2:

  • wolfSSL_CTX_set_options(ctx, WOLFSSL_OP_NO_TLSv1_3) only sets ctx->mask (src/ssl.c:6823); ctx->method->version stays at TLSv1_3.
  • wolfSSL_set_max_proto_version(ssl, TLS1_2_VERSION) -> Set_SSL_max_proto_version() only sets ssl->options.mask (src/ssl.c:7659) and leaves ssl->version at TLSv1_3. (wolfSSL_CTX_set_max_proto_version() does lower ctx->method->version.minor, so that ctx-level path is covered by the guard; these two are not.)

So with the macro enabled, SSL_CTX_set_options(ctx, WOLFSSL_OP_NO_TLSv1_3); SSL_CTX_set_ciphersuites(ctx, "TLS13-AES128-GCM-SHA256") passes the guard and SetCipherList_ex() installs the TLS 1.3-only suite list with suites->setSuites = 1. Measured against this PR head in a -DWOLFSSL_STRICT_CIPHER_LIST build (--enable-opensslextra --enable-tls13 --enable-debug): the call returns WOLFSSL_SUCCESS and leaves ctx->suites->suiteSz == 2, setSuites == 1, holding the single suite 0x1301 (TLS_AES_128_GCM_SHA256). Because TLS 1.3 is masked off at handshake time (src/internal.c:7089), the ctx now advertises only a suite it can never negotiate - a worse outcome than the silent-ignore the macro was added to eliminate, and exactly the misuse the caller enabled the macro to be protected from. A user who opts in reasonably concludes "a non-negotiable TLS 1.3-only list now returns WOLFSSL_FAILURE"; that conclusion holds only for the method-version path the added unit test exercises.

Recommendation: Extend the strict-mode predicate to consider the option mask as well as the method/ssl version, e.g. also treat (ctx != NULL && (ctx->mask & WOLFSSL_OP_NO_TLSv1_3)) and (ssl != NULL && (ssl->options.mask & WOLFSSL_OP_NO_TLSv1_3)) as "TLS 1.3 not negotiable". If that is deliberately out of scope, narrow the wording in src/internal.c and in the code comment to say the check is on the ctx/ssl method version only, so opt-in users do not over-trust the guarantee, and add a test for the SSL_OP_NO_TLSv1_3 case.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The strict-mode guard only checks ctx->method->version / ssl->version, not the WOLFSSL_OP_NO_TLSv1_3 option mask (SSL_CTX_set_options() / wolfSSL_set_max_proto_version()). Narrowed the doc comments to say so.

  • src/internal.c (~line 44): macro doc block now notes the check is version-only and doesn't catch WOLFSSL_OP_NO_TLSv1_3.
  • src/ssl.c (~line 3898): inline comment reworded to match.

Both are comment-only changes, no logic change.

Follow-up not done: extending the guard to also check the option mask, and adding a test for the SSL_OP_NO_TLSv1_3 case.

Comment thread tests/api/test_tls13.c
/* ctx->method caps the connection at TLS 1.2, so a cipher list that
* names only TLS 1.3 suites can never take effect on it. */
ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method()));
#ifdef WOLFSSL_STRICT_CIPHER_LIST

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 [Low] Strict branch of the new test is never compiled by any CI configuration
🔧 NIT test

The #ifdef WOLFSSL_STRICT_CIPHER_LIST half of the test only builds when a developer passes CPPFLAGS=-DWOLFSSL_STRICT_CIPHER_LIST by hand (as the PR description shows). No in-tree build config defines the macro, so the branch that the PR actually adds to src/ssl.c is dead code in every CI job and will silently rot. Several CI workflows already build with ad-hoc macro lists (e.g. .github/workflows/os-check.yml uses CPPFLAGS="-DWOLFSSL_..."), so there is an established place to add coverage.

Suggestion: Add -DWOLFSSL_STRICT_CIPHER_LIST to one existing opensslextra+tls13 CI matrix entry (e.g. in .github/workflows/os-check.yml) so the strict path is built and the new assertions actually run.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

No CI workflow defined WOLFSSL_STRICT_CIPHER_LIST, so the #ifdef WOLFSSL_STRICT_CIPHER_LIST branch of the new test in tests/api/test_tls13.c was dead code in every CI job — only the #else (silent-ignore) path ever ran. Piggybacked on the existing opensslextra-no-ca-names matrix entry instead of adding a new job, to avoid extra CI time.

  • .github/workflows/os-check.yml (~line 215-217): appended -DWOLFSSL_STRICT_CIPHER_LIST to that entry's existing CPPFLAGS.

Comment thread tests/api/test_tls13.c
return EXPECT_RESULT();
}

int test_tls13_cipher_list_no_tls13_ctx(void)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 [Low] New regression test's "suites unchanged" assertions are vacuous (0 == 0) and never verify preservation of configured suites
💡 SUGGEST

The non-strict branch claims to "Confirm the suite count is actually unchanged, not just that the call reported success", but no suites are ever configured before the measurement. ctx->suites is NULL right after wolfSSL_CTX_new(), so suiteSzBefore is 0; wolfSSL_CTX_set_cipher_list() then calls AllocateCtxSuites(), which XMEMSETs the new Suites to zero (src/internal.c:3651-3663), so the post-call suiteSz is also 0. The same holds for the ssl case: AllocateSuites() copies the (empty) ctx suites (src/internal.c:3666-3681). Both ExpectIntEQ(..., suiteSzBefore) checks therefore compare 0 with 0 and would still pass if the permissive path wiped a previously configured suite list. Symmetrically, the strict branch asserts only the WOLFSSL_FAILURE return and never checks that the prior suites survived the rejected call, which is the actual justification given for the new macro.

Recommendation: Configure a real <= TLS 1.2 list first (e.g. wolfSSL_CTX_set_cipher_list(ctx, "ECDHE-RSA-AES128-GCM-SHA256")), assert suiteSz > 0, snapshot the suite bytes, then issue the TLS 1.3-only call and compare both suiteSz and the suite bytes. Apply the same snapshot/compare in the WOLFSSL_STRICT_CIPHER_LIST branch so the "fail without mutating state" contract is actually tested.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

test_tls13_cipher_list_no_tls13_ctx measured suiteSzBefore right after wolfSSL_CTX_new()/wolfSSL_new(), before any suites were ever configured, so both the before and after suite counts were 0 — the assertion could never catch a regression that wiped a real, previously configured suite list. The strict branch didn't check state preservation at all.

Fixed in tests/api/test_tls13.c (test_tls13_cipher_list_no_tls13_ctx):

  • Configure a real <= TLS 1.2 suite (ECDHE-RSA-AES128-GCM-SHA256) first, assert suiteSz > 0, and snapshot the full Suites struct via XMEMCPY.
  • After the TLS 1.3-only call (both the strict-failure and non-strict-success paths, for both ctx and ssl), compare the current Suites struct against the snapshot with XMEMCMP instead of just comparing suiteSz.
  • Added build guards (!NO_RSA, HAVE_ECC, !NO_AES, HAVE_AESGCM, !NO_SHA256, BUILD_TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256) needed for the new TLS 1.2 suite name, matching the convention used elsewhere in tests/api/test_tls.c.

Verified the modified file compiles cleanly (no errors, no unused-variable warnings under -Wall -Wextra -Wunused-variable) using this repo's actual AM_CFLAGS, confirming the new branch is genuinely active (not compiled out) in this build configuration.

miyazakh added 2 commits July 31, 2026 13:56
Suites has a trailing bitfield (setSuites:1) followed by indeterminate
compiler padding, so comparing the whole struct via XMEMCMP is
unreliable. Replace the whole-struct comparisons with a member-wise
helper.
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.

4 participants