Add WOLFSSL_STRICT_CIPHER_LIST for TLS 1.3-only cipher lists on a non-TLS1.3 ctx/ssl - #10963
Add WOLFSSL_STRICT_CIPHER_LIST for TLS 1.3-only cipher lists on a non-TLS1.3 ctx/ssl#10963miyazakh wants to merge 5 commits into
Conversation
There was a problem hiding this comment.
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_LISTto 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.
|
|
retest this please |
3 similar comments
|
retest this please |
|
retest this please |
|
retest this please |
Frauschi
left a comment
There was a problem hiding this comment.
🐺 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 path —
tests/api/test_tls13.c:2466-2489 - [Low] Test asserts return value but not that suites are actually left untouched —
tests/api/test_tls13.c:2480-2484 - [Low] WOLFSSL_STRICT_CIPHER_LIST can now fail ctx creation under WOLFSSL_SYS_CRYPTO_POLICY —
src/ssl.c:636-641
Review generated by Skoll via Claude/Codex
|
Jenkins retest this please. |
Frauschi
left a comment
There was a problem hiding this comment.
🐺 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 semantics —
src/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 detected —
src/ssl.c:3891-3910 - [Low] Strict branch of the new test is never compiled by any CI configuration —
tests/api/test_tls13.c:2481-2483 - [Low] New regression test's "suites unchanged" assertions are vacuous (0 == 0) and never verify preservation of configured suites —
tests/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
| tls13Only = 1; | ||
| if ((ctx != NULL && !IsAtLeastTLSv1_3(ctx->method->version)) || | ||
| (ssl != NULL && !IsAtLeastTLSv1_3(ssl->version))) { | ||
| #ifdef WOLFSSL_STRICT_CIPHER_LIST |
There was a problem hiding this comment.
🟡 [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:
| #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). ... |
There was a problem hiding this comment.
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().
| @@ -3895,8 +3895,17 @@ static int wolfSSL_parse_cipher_list(WOLFSSL_CTX* ctx, WOLFSSL* ssl, | |||
| tls13Only = 1; | |||
There was a problem hiding this comment.
🟡 [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 setsctx->mask(src/ssl.c:6823);ctx->method->versionstays at TLSv1_3.wolfSSL_set_max_proto_version(ssl, TLS1_2_VERSION)->Set_SSL_max_proto_version()only setsssl->options.mask(src/ssl.c:7659) and leavesssl->versionat TLSv1_3. (wolfSSL_CTX_set_max_proto_version()does lowerctx->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.
There was a problem hiding this comment.
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 catchWOLFSSL_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.
| /* 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 |
There was a problem hiding this comment.
🔵 [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.
There was a problem hiding this comment.
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_LISTto that entry's existingCPPFLAGS.
| return EXPECT_RESULT(); | ||
| } | ||
|
|
||
| int test_tls13_cipher_list_no_tls13_ctx(void) |
There was a problem hiding this comment.
🔵 [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.
There was a problem hiding this comment.
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.2suite (ECDHE-RSA-AES128-GCM-SHA256) first, assertsuiteSz > 0, and snapshot the fullSuitesstruct viaXMEMCPY. - After the TLS 1.3-only call (both the strict-failure and non-strict-success paths, for both
ctxandssl), compare the currentSuitesstruct against the snapshot withXMEMCMPinstead of just comparingsuiteSz. - 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 intests/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.
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.
Summary(f3228)
wolfSSL_CTX_set_cipher_list()/wolfSSL_set_cipher_list()silentlyreturn
WOLFSSL_SUCCESSwithout touching the configured suites whengiven a cipher list that names only TLS 1.3 suites, but the
ctx/sslcannot negotiate TLS 1.3 (
OPENSSL_EXTRAbuilds only, viawolfSSL_parse_cipher_list()insrc/ssl.c).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.2suites were already configured (default or otherwise).
WOLFSSL_STRICT_CIPHER_LISTbuild macro. When defined,the function returns
WOLFSSL_FAILUREinstead of silently ignoringthe 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 inwolfSSL_parse_cipher_list()behind
WOLFSSL_STRICT_CIPHER_LIST.src/internal.c: document the new macro in the build-options headercomment (Cipher Suite Selection section).
.wolfssl_known_macro_extras: registerWOLFSSL_STRICT_CIPHER_LIST.tests/api/test_tls13.c,tests/api/test_tls13.h: addtest_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_LISTundefined):
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_ctxpasses in bothconfigurations, exercising
wolfSSL_CTX_set_cipher_list(ctx, "TLS13-AES128-GCM-SHA256")on awolfTLSv1_2_client_method()context.
Checklist