Specify which documentation you found a problem with
https://curl.se/docs/ssl-ciphers.html
The problem
curl \
--tlsv1.3 \
--tls13-ciphers TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256 \
https://example.com/
Restrict to only TLS 1.3 with aes128-gcm and chacha20 ciphers. Works with OpenSSL, LibreSSL, mbedTLS, wolfSSL and Schannel.
tlsv1.3 is not supported by Schannel. See the below "security report" which I decided not to report as a security report.
Hi all,
We've found an issue in lib/vtls/schannel.c where setting CURLOPT_SSL_CIPHER_LIST (or --ciphers) routes the request through the SCHANNEL_CRED credential structure (e.g. https://curl.se/docs/ssl-ciphers.html "Restrict to only TLS 1.3 with aes128-gcm and chacha20 ciphers. Works with OpenSSL, LibreSSL, mbedTLS, wolfSSL and Schannel".) When the user has pinned TLS 1.3 as the only allowed version, the handshake completes at TLS 1.2 with CURLE_OK and only an infof advisory.
The branch
lib/vtls/schannel.c:595-600:
if(!conn_config->cipher_list &&
curlx_verify_windows_version(10, 0, 17763, PLATFORM_WINNT,
VERSION_GREATER_THAN_EQUAL)) {
/* modern SCH_CREDENTIALS path — supports TLS 1.3 */
...
}
else {
/* legacy SCHANNEL_CRED path — TLS 1.2 max */
...
}
Inside the else branch at lines 655-660, if enabled_protocols & SP_PROT_TLS1_3_CLIENT is set, the code emits an infof warning and falls through. No failf, no error code, handshake proceeds at TLS 1.2 -- https://github.com/curl/curl/blob/master/lib/vtls/schannel.c#L655-L660.
Compare with lib/vtls/schannel.c:187-198: when TLS 1.3 is requested on a Windows version that can't negotiate it, that path returns CURLE_SSL_CONNECT_ERROR. The cipher-list-conflict path warrants the same treatment, but only when TLS 1.2 was not also allowed.
When this fires
schannel_set_ssl_version_min_max at lib/vtls/schannel.c:176 builds the enabled-protocols mask by iterating from the user's min version up to the max. The bug is when that mask ends up containing TLS 1.3 only — i.e. the user disallowed TLS 1.2 — and a cipher list is also set:
CURLOPT_SSLVERSION = TLSv1_3 | MAX_TLSv1_3 (pinned) → mask = 1.3 only → policy violation.
CURLOPT_SSLVERSION = TLSv1_3 on Win11 / Server 2022+ (default max is TLS 1.3) → mask = 1.3 only → policy violation.
CURLOPT_SSLVERSION = TLSv1_2 | MAX_TLSv1_3 (range) → mask = 1.2 | 1.3 → within policy; warning is appropriate.
The patch below targets only the first two.
infof is not visibility
There is an infof output when this happens, but it is delivered only to callers that set CURLOPT_VERBOSE or install CURLOPT_DEBUGFUNCTION. Policy-driven curl invocations (PCI-DSS, HIPAA hardening profiles pinning TLS 1.3 plus a constrained cipher list) do not see it. The user's only signal is curl_easy_perform returning CURLE_OK.
Origin
Commit b4f9ae5126 (2023-03-19, "schannel: fix user-set legacy algorithms in Windows 10 & 11") restored legacy-cipher support by routing requests with a cipher list through SCHANNEL_CRED. The TLS-1.3 silent downgrade is a side-effect of that routing decision and was not addressed by the follow-up commit 6238888ca7 ("schannel: remove TLS 1.3 ciphersuite-list support"), which handles the separate CURLOPT_TLS13_CIPHERS collision.
Reproducer
Build curl with cmake -DCURL_USE_SCHANNEL=ON. Save as poc.c:
#include <stdio.h>
#include <curl/curl.h>
int main(int argc, char **argv) {
curl_global_init(CURL_GLOBAL_DEFAULT);
CURL *e = curl_easy_init();
curl_easy_setopt(e, CURLOPT_URL, argv[1]);
curl_easy_setopt(e, CURLOPT_SSLVERSION,
(long)(CURL_SSLVERSION_TLSv1_3 | CURL_SSLVERSION_MAX_TLSv1_3));
curl_easy_setopt(e, CURLOPT_SSL_CIPHER_LIST,
"ECDHE-ECDSA-AES128-GCM-SHA256");
CURLcode rc = curl_easy_perform(e);
long ver = 0;
curl_easy_getinfo(e, CURLINFO_SSL_VERSION, &ver);
printf("perform=%d (%s) — version=0x%lx\n",
rc, curl_easy_strerror(rc), ver);
curl_easy_cleanup(e);
curl_global_cleanup();
return 0;
}
The output is perform=0 with the wire connection actually negotiated at TLS 1.2, despite the user having requested only TLS 1.3.
Suggested fix
Fail in the legacy else branch only when TLS 1.3 is the only enabled protocol — preserving the existing infof-only behavior for the legitimate mixed-version case:
--- a/lib/vtls/schannel.c
+++ b/lib/vtls/schannel.c
@@ -598,6 +598,17 @@
if(!conn_config->cipher_list &&
curlx_verify_windows_version(10, 0, 17763, PLATFORM_WINNT,
VERSION_GREATER_THAN_EQUAL)) {
+ /* modern SCH_CREDENTIALS path — supports TLS 1.3 */
SCH_CREDENTIALS credentials = { 0 };
...
}
else {
+ /* legacy SCHANNEL_CRED cannot negotiate TLS 1.3. If TLS 1.3 is the only
+ protocol the user allowed, do not silently downgrade. */
+ if((enabled_protocols & SP_PROT_TLS1_3_CLIENT) &&
+ !(enabled_protocols & (SP_PROT_TLS1_2_CLIENT |
+ SP_PROT_TLS1_1_CLIENT |
+ SP_PROT_TLS1_0_CLIENT))) {
+ failf(data, "schannel: TLS 1.3 is the only enabled version but cannot "
+ "be negotiated together with CURLOPT_SSL_CIPHER_LIST; remove the "
+ "cipher list or allow TLS 1.2 in the version range.");
+ return CURLE_SSL_CONNECT_ERROR;
+ }
SCHANNEL_CRED credentials = { 0 };
...
}
Cheers,
AISLE Research Team
Specify which documentation you found a problem with
https://curl.se/docs/ssl-ciphers.html
The problem
tlsv1.3 is not supported by Schannel. See the below "security report" which I decided not to report as a security report.
Hi all,
We've found an issue in
lib/vtls/schannel.cwhere settingCURLOPT_SSL_CIPHER_LIST(or--ciphers) routes the request through theSCHANNEL_CREDcredential structure (e.g. https://curl.se/docs/ssl-ciphers.html "Restrict to only TLS 1.3 with aes128-gcm and chacha20 ciphers. Works with OpenSSL, LibreSSL, mbedTLS, wolfSSL and Schannel".) When the user has pinned TLS 1.3 as the only allowed version, the handshake completes at TLS 1.2 withCURLE_OKand only aninfofadvisory.The branch
lib/vtls/schannel.c:595-600:Inside the
elsebranch at lines 655-660, ifenabled_protocols & SP_PROT_TLS1_3_CLIENTis set, the code emits aninfofwarning and falls through. Nofailf, no error code, handshake proceeds at TLS 1.2 -- https://github.com/curl/curl/blob/master/lib/vtls/schannel.c#L655-L660.Compare with
lib/vtls/schannel.c:187-198: when TLS 1.3 is requested on a Windows version that can't negotiate it, that path returnsCURLE_SSL_CONNECT_ERROR. The cipher-list-conflict path warrants the same treatment, but only when TLS 1.2 was not also allowed.When this fires
schannel_set_ssl_version_min_maxatlib/vtls/schannel.c:176builds the enabled-protocols mask by iterating from the user's min version up to the max. The bug is when that mask ends up containing TLS 1.3 only — i.e. the user disallowed TLS 1.2 — and a cipher list is also set:CURLOPT_SSLVERSION = TLSv1_3 | MAX_TLSv1_3(pinned) → mask =1.3only → policy violation.CURLOPT_SSLVERSION = TLSv1_3on Win11 / Server 2022+ (default max is TLS 1.3) → mask =1.3only → policy violation.CURLOPT_SSLVERSION = TLSv1_2 | MAX_TLSv1_3(range) → mask =1.2 | 1.3→ within policy; warning is appropriate.The patch below targets only the first two.
infofis not visibilityThere is an
infofoutput when this happens, but it is delivered only to callers that setCURLOPT_VERBOSEor installCURLOPT_DEBUGFUNCTION. Policy-driven curl invocations (PCI-DSS, HIPAA hardening profiles pinning TLS 1.3 plus a constrained cipher list) do not see it. The user's only signal iscurl_easy_performreturningCURLE_OK.Origin
Commit
b4f9ae5126(2023-03-19, "schannel: fix user-set legacy algorithms in Windows 10 & 11") restored legacy-cipher support by routing requests with a cipher list throughSCHANNEL_CRED. The TLS-1.3 silent downgrade is a side-effect of that routing decision and was not addressed by the follow-up commit6238888ca7("schannel: remove TLS 1.3 ciphersuite-list support"), which handles the separateCURLOPT_TLS13_CIPHERScollision.Reproducer
Build curl with
cmake -DCURL_USE_SCHANNEL=ON. Save aspoc.c:The output is
perform=0with the wire connection actually negotiated at TLS 1.2, despite the user having requested only TLS 1.3.Suggested fix
Fail in the legacy
elsebranch only when TLS 1.3 is the only enabled protocol — preserving the existing infof-only behavior for the legitimate mixed-version case:Cheers,
AISLE Research Team