Skip to content

ssl-ciphers says TLS1.3 is supported on Schannel #21702

@MegaManSec

Description

@MegaManSec

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions