Repository navigation
Leave objects unchanged when version, group and sigalgs setters fail - #11938
julek-wolfssl wants to merge 5 commits into
Conversation
Protocol version setters: - wolfSSL_CTX_set_min_proto_version(), wolfSSL_CTX_set_max_proto_version(), wolfSSL_set_min_proto_version() and wolfSSL_set_max_proto_version() work on a copy of the version limits and store it only on success. A failing call could leave every version disabled, or enable again versions that the application had disabled with options. - The CTX setters record the NO_* options each of them sets and drop only those, so options set by the application are kept and a later call can lower the min or raise the max. The max setter no longer re-applies the min. wolfSSL_CTX_set_options(), wolfSSL_CTX_clear_options() and wolfSSL_CTX_SetMinVersion() update the records. - A call fails when it leaves no version from the min up, counting a min held only in minDowngrade: the default one, one set with wolfSSL_CTX_SetMinVersion() or wolfSSL_SetMinVersion(), or the crypto policy's. In old TLS builds without TLS 1.0, a TLS 1.0 min is TLS 1.1. - A DTLS min on a TLS CTX fails, and so does an SSL max of the other major. - The CTX max setter rebuilds ctx->suites when it changes the method version. - wolfSSL_CTX_SetMinVersion() clears the automatic min flag. - WOLFSSL_MIN_DOWNGRADE is TLS 1.1 in old TLS builds without WOLFSSL_ALLOW_TLSV10. - With WOLFSSL_SYS_CRYPTO_POLICY at level 0, including when no policy is loaded, a new TLS CTX keeps the default min instead of an SSLv3 min. Cipher suites: - The CTX PSK callback setters and wolfSSL_CTX_allow_anon_cipher() rebuild ctx->suites when it was already derived. The max setter no longer leaves the min's options in ctx->mask, so SSLs created after a late PSK callback shared stale CTX suites without PSK. A cipher list set by the application is kept. Groups: - wolfSSL_CTX_set_groups() and wolfSSL_set_groups() build the Supported Groups extension in a local list and replace the old one only on success. They move from tls.c to ssl_api_ext.c. - set_curves_list() sets the groups once, after parsing. - wolfSSL_UseSupportedCurve() and wolfSSL_CTX_UseSupportedCurve() set userCurves only when the group was added. Signature algorithms: - SetSuitesHashSigAlgo() parses into a local buffer and copies it only when the whole list parses. - wolfSSL_CTX_set1_sigalgs_list() on a CTX without suites frees the suites it allocated when the list is rejected, and adds the default cipher suites when it is accepted.
|
There was a problem hiding this comment.
🟢 Approval recommended
No active review issues are reported for the setter-state preservation and derived cipher-suite consistency changes.
No open issues 🎉
What changed in this PR
This PR makes wolfSSL protocol-version, group, and signature-algorithm setters preserve existing state when a call fails. It also keeps derived cipher suites consistent with later configuration changes.
Changes:
- Apply protocol limits only on success while preserving application-set options and default minimum versions.
- Validate group and signature-algorithm lists before replacing existing settings.
- Refresh derived cipher suites after relevant configuration changes and add regression tests for state preservation and handshake behavior.
| File | Description |
|---|---|
wolfssl/internal.h |
Track setter-owned protocol options and adjust the default minimum version. |
tests/api.c |
Add regression coverage for failed setters, option preservation, and cipher-suite updates. |
src/tls13.c |
Refresh derived CTX suites when TLS 1.3 PSK callbacks change. |
src/tls.c |
Remove group setters relocated to ssl_api_ext.c. |
src/ssl.c |
Stage protocol-limit changes, track option ownership, refresh suites, and defer group updates until parsing completes. |
src/ssl_api_ext.c |
Replace groups only on success and handle fresh signature-algorithm suite allocation. |
src/internal.c |
Preserve signature algorithms on parse failure and correct the crypto-policy default minimum. |
Review effort: Balanced
In builds without TLS 1.3, wolfSSL_CTX_set_max_proto_version() with TLS1_3_VERSION or 0 set the method version to TLS 1.3. Clients then handled TLS 1.2 handshake messages as TLS 1.3 and failed with BUFFER_E (test_wolfSSL_CTX_set_psk_client_callback_suites with --enable-wpas --disable-tls13).
|
@claude review |
| if (ctx->suites != NULL && lim.maxMinor != minor) { | ||
| /* rebuild the suites derived for the previous max */ | ||
| InitSSL_CTX_Suites(ctx, ctx->suites); |
There was a problem hiding this comment.
🔴 SSL objects created before a CTX max-version, PSK-callback or anon call now have their cipher list changed under them, so pre-created TLS 1.3 handshakes can fail. These setters now rebuild ctx->suites in place. Every SSL whose own ssl->suites is NULL reads that same buffer through WOLFSSL_SUITES(). Fix: a rebuild must not change the suites of SSLs that already exist. One way is to rebuild only when no SSL holds the CTX, or to give each SSL its own copy at wolfSSL_new; the fully durable fix is larger than this change. Same pattern at 7 sites (ssl.c:5466, ssl.c:4079, ssl.c:4161, ssl.c:4282, tls13.c:17711, tls13.c:17767, tls13.c:17821).
Why this was flagged
An application calls wolfSSL_new(ctx) on a TLS 1.3-capable client CTX. That SSL copies ssl->version (1.3) and ssl->options.mask, but keeps ssl->suites NULL, so WOLFSSL_SUITES(ssl) at wolfssl/internal.h:6847 reads ctx->suites. The application then calls wolfSSL_CTX_set_max_proto_version(ctx, TLS1_2_VERSION) so that later connections use TLS 1.2. With this change, ssl.c:5464-5466 runs InitSSL_CTX_Suites(ctx, ctx->suites). That rewrites the shared buffer in place and drops every TLS 1.3 suite. The SSL created earlier still advertises TLS 1.3 in supported_versions, but its ClientHello now carries only TLS 1.2 suites. A TLS 1.3 server then finds no common TLS 1.3 cipher and the handshake fails. On the base branch the max setter changed only ctx->method->version.minor and ctx->mask, so SSLs that already existed kept their suites. The setSuites early return in InitSuites (internal.c:4577) protects only cipher lists the application set itself, not the derived default suites.
Verification: WOLFSSL_SUITES(ssl) returns (ssl)->ctx->suites (internal.h:6847-6850). With this change, wolfSSL_CTX_set_max_proto_version calls InitSSL_CTX_Suites(ctx, ctx->suites) (ssl.c:5463-5467), which rewrites the shared Suites buffer in place. The base max setter never touched ctx->suites, so an SSL created earlier kept its TLS 1.3 suites and connected.
| /* A min above the max drops the NO_* options set for the max. i is the | ||
| * table index of the min and idx the one of the max. */ | ||
| maxProto = ctx->maxProto ? 0 : GetMaxProtoVersion((long)lim.mask); | ||
| for (idx = 0; (unsigned)idx < NUMBER_OF_PROTOCOLS; idx++) { | ||
| if (protoVerTbl[idx] == maxProto) { | ||
| break; | ||
| } | ||
| } | ||
| if (idx < i) { | ||
| wolfSSL_CTX_clear_options(ctx, WOLFSSL_OP_NO_TLSv1 | | ||
| WOLFSSL_OP_NO_TLSv1_1 | WOLFSSL_OP_NO_TLSv1_2 | | ||
| WOLFSSL_OP_NO_TLSv1_3); | ||
| lim.mask &= ~lim.maxProtoMask; | ||
| lim.maxProtoMask = 0; | ||
| } |
There was a problem hiding this comment.
🟣 pre-existing, not blocking: Pre-existing: raising the min above an earlier max leaves the CTX advertising the old max, so handshakes fail even though the call returns success. When idx < i, the min setter drops maxProtoMask but keeps lim.maxMinor, so ctx->method->version.minor keeps the old max. ctx->suites is not rebuilt either. Fix: when the max's options are dropped, also reset lim.maxMinor to the highest version still enabled. Then rebuild ctx->suites as wolfSSL_CTX_set_max_proto_version does at ssl.c:5466. [also at: src/ssl.c:5466 - pre-existing: A CTX whose max is TLS 1.2 still accepts a TLS 1.3 min, and its connections then fail. When the min is above the max, wolfSSL_CTX_set_min_proto_version drops only the max's NO_* bits (lim.maxProtoMask).]
A small fix can ride a push you are already making; otherwise a short reply is enough.
Why this was flagged
An application calls wolfSSL_CTX_set_max_proto_version(ctx, TLS1_1_VERSION), which sets method->version.minor = TLSv1_1_MINOR and maxProtoMask = NO_TLSv1_2|NO_TLSv1_3. It then calls wolfSSL_CTX_set_min_proto_version(ctx, TLS1_3_VERSION). At ssl.c:5337 idx (TLS1_1) < i (TLS1_3), so ssl.c:5338-5339 clear the max's NO_* bits. setCtxProtoLimits at ssl.c:5344 still writes back the unchanged lim.maxMinor (TLSv1_1_MINOR). The call returns WOLFSSL_SUCCESS, and wolfSSL_CTX_get_max_proto_version reports TLS1_3 from the mask. SSLs created from the CTX still start at version 1.1, and their minDowngrade is TLSv1_3_MINOR, so every handshake fails with a version error. The base had the same stale-minor behaviour, so this is pre-existing. The PR's claim that a later call can raise the max does not cover this path. The new test at tests/api.c only passes because it calls set_max_proto_version(TLS1_3_VERSION) afterwards.
Verification: Pre-existing. Trigger: an app sets a CTX max and then sets a min above it (TLS1_3_VERSION). ssl.c:5337-5339 clear only lim.mask &= ~lim.maxProtoMask; lim.maxProtoMask = 0;. lim.maxMinor is never touched. setCtxProtoLimits (ssl.c:5344) writes back maxMinor = TLSv1_1_MINOR, the call returns WOLFSSL_SUCCESS, and ctx->suites is not rebuilt. The base had the same behaviour by the same route.
| switch (version) { | ||
| #ifndef NO_TLS | ||
| case SSL3_VERSION: | ||
| #if defined(WOLFSSL_ALLOW_SSLV3) && !defined(NO_OLD_TLS) | ||
| ctx->minDowngrade = SSLv3_MINOR; | ||
| lim->minDowngrade = SSLv3_MINOR; | ||
| break; | ||
| #endif | ||
| case TLS1_VERSION: | ||
| #ifdef WOLFSSL_ALLOW_TLSV10 | ||
| ctx->minDowngrade = TLSv1_MINOR; | ||
| lim->minDowngrade = TLSv1_MINOR; | ||
| break; | ||
| #endif | ||
| case TLS1_1_VERSION: | ||
| #ifndef NO_OLD_TLS | ||
| ctx->minDowngrade = TLSv1_1_MINOR; | ||
| lim->minDowngrade = TLSv1_1_MINOR; | ||
| break; | ||
| #endif | ||
| case TLS1_2_VERSION: | ||
| #ifndef WOLFSSL_NO_TLS12 | ||
| ctx->minDowngrade = TLSv1_2_MINOR; | ||
| lim->minDowngrade = TLSv1_2_MINOR; | ||
| break; | ||
| #endif | ||
| case TLS1_3_VERSION: | ||
| #ifdef WOLFSSL_TLS13 | ||
| ctx->minDowngrade = TLSv1_3_MINOR; | ||
| lim->minDowngrade = TLSv1_3_MINOR; | ||
| break; | ||
| #endif | ||
| #endif | ||
| #ifdef WOLFSSL_DTLS | ||
| case DTLS1_VERSION: | ||
| #ifndef NO_OLD_TLS | ||
| ctx->minDowngrade = DTLS_MINOR; | ||
| lim->minDowngrade = DTLS_MINOR; | ||
| break; | ||
| #endif | ||
| case DTLS1_2_VERSION: | ||
| ctx->minDowngrade = DTLSv1_2_MINOR; | ||
| lim->minDowngrade = DTLSv1_2_MINOR; | ||
| break; | ||
| #endif | ||
| default: | ||
| WOLFSSL_MSG("Unrecognized protocol version or not compiled in"); | ||
| return WOLFSSL_FAILURE; | ||
| } | ||
|
|
||
| switch (version) { | ||
| #ifndef NO_TLS | ||
| case TLS1_3_VERSION: | ||
| wolfSSL_CTX_set_options(ctx, WOLFSSL_OP_NO_TLSv1_2); | ||
| FALL_THROUGH; | ||
| case TLS1_2_VERSION: | ||
| wolfSSL_CTX_set_options(ctx, WOLFSSL_OP_NO_TLSv1_1); | ||
| FALL_THROUGH; | ||
| case TLS1_1_VERSION: | ||
| wolfSSL_CTX_set_options(ctx, WOLFSSL_OP_NO_TLSv1); | ||
| FALL_THROUGH; | ||
| case TLS1_VERSION: | ||
| wolfSSL_CTX_set_options(ctx, WOLFSSL_OP_NO_SSLv3); | ||
| break; | ||
| case SSL3_VERSION: | ||
| case SSL2_VERSION: | ||
| /* Nothing to do here */ | ||
| break; | ||
| #endif | ||
| #ifdef WOLFSSL_DTLS | ||
| case DTLS1_VERSION: | ||
| case DTLS1_2_VERSION: | ||
| break; | ||
| #endif | ||
| default: | ||
| WOLFSSL_MSG("Unrecognized protocol version or not compiled in"); | ||
| return WOLFSSL_FAILURE; | ||
| if ((version >> 8) == SSLv3_MAJOR) { | ||
| disableProto(lim, &lim->minProtoMask, tlsOptionsBelow((byte)version)); |
There was a problem hiding this comment.
🟣 pre-existing, not blocking: pre-existing: DTLS users who call the min setter with 0 or a TLS version end up with a TLS minor in minDowngrade, so version checks reject DTLS 1.2 peers. setMinProtoVersion accepts SSL3_VERSION..TLS1_3_VERSION whatever lim->major is, and checkProtoLimits always succeeds for DTLS_MAJOR. With 0, the protoVerTbl loop stops at SSL3_VERSION and stores TLSv1_1_MINOR. Fix: in setMinProtoVersion, reject a version whose major is not lim->major, covering the CTX and SSL setters. The 0 loop then falls through to DTLS1_VERSION/DTLS1_2_VERSION.
A small fix can ride a push you are already making; otherwise a short reply is enough.
Why this was flagged
wolfSSL_CTX_set_min_proto_version(ctx, 0) on a wolfDTLS_method ctx loops over protoVerTbl (ssl.c:5086). SSL3_VERSION comes first. Without WOLFSSL_ALLOW_SSLV3 it falls through to lim->minDowngrade = TLSv1_1_MINOR (2). checkProtoLimits (ssl.c:5195) only adds TLS options for SSLv3_MAJOR, and CheckSslMethodVersion returns success for DTLS_MAJOR, so 2 is committed. DTLS compares in reverse: internal.c:36257 and internal.c:43018 use pv.minor > minDowngrade. DTLSv1_2_MINOR (0xfd) > 2, so a downgrade-capable DTLS client or server rejects every peer version. wolfSSL_CTX_set_min_proto_version(dtls_ctx, TLS1_2_VERSION) and wolfSSL_set_min_proto_version on a DTLS SSL hit the same path. The base Set_CTX_min_proto_version and Set_SSL_min_proto_version did the same, so this is pre-existing. The new DTLS_MAJOR check at ssl.c:5297 only covers the opposite case, a DTLS version on a TLS ctx.
Verification: pre-existing. Trigger: an app calls wolfSSL_CTX_set_min_proto_version(ctx, 0) on a wolfDTLS*_method CTX. setMinProtoVersion in src/ssl.c lands on lim->minDowngrade = TLSv1_1_MINOR;, and CheckSslMethodVersion's case DTLS_MAJOR: sanityConfirmed = 1; always succeeds. Any DTLS minor (0xfd/0xff) is > 2, so every DTLS downgrade is rejected with VERSION_ERROR (internal.c:36256-36257).
SparkiDev
left a comment
There was a problem hiding this comment.
Doxygen for the min/max setters: behaviour that still differs from OpenSSL
OpenSSL accepts both of these and fails at handshake time. wolfSSL rejects them and leaves the object unchanged:
- setting a max below the min;
- setting a min that leaves only versions already disabled by SSL_OP_NO_*.
tests/api.c: coverage gaps
- There are no tests for successful SSL-level sequences, such as raising and lowering the SSL max/min and then doing a handshake. Comment 1 would have been caught by one.
- The suites test only raises the max; nothing tests lowering it (1.3 → 1.2) after the suites were built.
- The server PSK callback and the three TLS 1.3 PSK callbacks have no tests for their new rebuild path.
- No test creates an SSL first, then changes the CTX max/PSK/anon, then does a handshake with that SSL. That's the case from the in-place rebuild comment.
- There's no test of a TLS version or 0 passed to the min setter on a DTLS ctx and ssl, the case from the major-version comment.
- test_wolfSSL_CTX_allow_anon_cipher_suites needs --enable-anon, and test_wolfSSL_crypto_policy_default_min needs a sys-crypto-policy build.
|
|
||
| /* An SSL does not record the options its setters set, so they only add | ||
| * options. */ | ||
| static void getSslProtoLimits(const WOLFSSL* ssl, ProtoLimits* lim) |
There was a problem hiding this comment.
an SSL max can't be raised, yet the setter reports success
getSslProtoLimits() sets minProtoMask/maxProtoMask to 0, so the SSL setters only ever add NO_* options. wolfSSL_set_max_proto_version(ssl, TLS1_2_VERSION) followed by wolfSSL_set_max_proto_version(ssl, TLS1_3_VERSION) (or 0) returns WOLFSSL_SUCCESS, but TLS 1.3 stays disabled and the handshake uses TLS 1.2. OpenSSL negotiates TLS 1.3 here.
It has become more visible in this PR. With max 1.2, then min 1.3, then max 1.3 on an SSL, master returned failure on the last call. This branch returnssuccess and still negotiates TLS 1.2.
Could ssl->options record the min/max options the way the CTX now does? It could copy them from the CTX in InitSSL. If that's out of scope, the max setter should at least fail when the requested max is above a version that is still masked off, rather than report success.
| setCtxProtoLimits(ctx, &lim); | ||
| ctx->maxProto = (version == 0); | ||
| if (ctx->suites != NULL && lim.maxMinor != minor) { | ||
| /* rebuild the suites derived for the previous max */ |
There was a problem hiding this comment.
rebuilding the suites keeps the signature algorithms derived for the old max
InitSuites() refills hashSigAlgo only when hashSigAlgoSz == 0. After raising the max from 1.2 to 1.3, the cipher suites are rebuilt but the sigalgs stay the TLS 1.2 list. With HAVE_ECC_BRAINPOOL, the TLS 1.3-only brainpool sigalgs are missing. Clear hashSigAlgoSz before rebuilding unless the user set the sigalgs. There's no flag today that says which, so one is needed.
Whatever replaces the in-place rebuild flagged in the earlier comment on this line should derive the sigalgs again too.
| #define WOLFSSL_MIN_DOWNGRADE TLSv1_MINOR | ||
| #elif !defined(NO_OLD_TLS) |
There was a problem hiding this comment.
the default minimum version changes
With !NO_OLD_TLS && !WOLFSSL_ALLOW_TLSV10, WOLFSSL_MIN_DOWNGRADE is now TLS 1.1 instead of TLS 1.0. That's more accurate, but it's visible to users: wolfSSL_CTX_get_min_proto_version() on a fresh ctx now returns TLS1_1_VERSION. It also affects wolfSSL_crypto_policy_init_ctx(). Please add it to the PR description or ChangeLog.
|
|
||
| ctx->mask = (unsigned long)wolf_set_options((long)ctx->mask, opt); | ||
| #ifdef OPENSSL_EXTRA | ||
| /* Versions disabled here belong to the application */ |
There was a problem hiding this comment.
wolfSSL_CTX_set_options takes over the setters' recorded options
Clearing minProtoMask/maxProtoMask here means that if the app sets NO_TLSv1_2 while a max already disabled it, the option becomes the app's. A later set_max_proto_version(TLS1_3) then keeps TLS 1.2 off. That's intentional and tested, but OpenSSL keeps options and min/max completely separate. Please state the precedence rule in the doxygen for wolfSSL_CTX_set_min_proto_version()/wolfSSL_CTX_set_max_proto_version().
The max, PSK and anon CTX setters rebuild ctx->suites in place, but kept the sigalgs derived for the old max. Derive them again unless the user set them, which a new Suites flag records.
- SSL objects record the NO_* options their min and max setters set, copied from the CTX, so a later call can raise an SSL max again. The SSL version is raised up to the CTX max. An SSL max or min above the CTX max fails, as the handshake treats the CTX max as the highest version. - A min above the max makes the highest version left the max, so the method version no longer keeps the dropped max. - A min of the other major fails. An automatic min on DTLS now picks a DTLS version.
|
Pushed three commits addressing the review:
|
Several wolfSSL setters could leave a CTX or SSL half-modified when they failed. This change makes them apply their result only on success, and fixes related cipher suite and option handling.
Protocol versions
NO_*options they set and drop only those, so application options are kept and a later call can lower the min or raise the max. The max setter no longer re-applies the min.wolfSSL_CTX_set_options(),wolfSSL_CTX_clear_options()andwolfSSL_CTX_SetMinVersion()update the records.minDowngrade(the default, one set withwolfSSL_CTX_SetMinVersion()orwolfSSL_SetMinVersion(), or the crypto policy's). In old TLS builds without TLS 1.0, a TLS 1.0 min is TLS 1.1.ctx->suiteswhen it changes the method version.wolfSSL_CTX_SetMinVersion()clears the automatic min flag.WOLFSSL_MIN_DOWNGRADEis TLS 1.1 in old TLS builds withoutWOLFSSL_ALLOW_TLSV10.WOLFSSL_SYS_CRYPTO_POLICYat level 0, including when no policy is loaded, a new TLS CTX keeps the default min instead of an SSLv3 min.Cipher suites
wolfSSL_CTX_allow_anon_cipher()rebuildctx->suiteswhen it was already derived. The max setter no longer leaves the min's options inctx->mask. Previously, SSLs created after a late PSK callback shared stale CTX suites without PSK. A cipher list set by the application is kept.Groups
wolfSSL_CTX_set_groups()andwolfSSL_set_groups()build the Supported Groups extension in a local list and replace the old one only on success. They move fromtls.ctossl_api_ext.c.set_curves_list()sets the groups once, after parsing.wolfSSL_UseSupportedCurve()andwolfSSL_CTX_UseSupportedCurve()setuserCurvesonly when the group was added.Signature algorithms
SetSuitesHashSigAlgo()parses into a local buffer and copies it only when the whole list parses.wolfSSL_CTX_set1_sigalgs_list()on a CTX without suites frees the suites it allocated when the list is rejected, and adds the default cipher suites when it is accepted.ZD22696