]> git.ipfire.org Git - thirdparty/curl.git/commitdiff
gnutls: set priority via --ciphers
authorStefan Eissing <stefan@eissing.org>
Tue, 4 Mar 2025 14:50:12 +0000 (15:50 +0100)
committerDaniel Stenberg <daniel@haxx.se>
Wed, 5 Mar 2025 12:51:56 +0000 (13:51 +0100)
No longer ignore the `--ciphers` argument in gnutls curl builds, but use
it to set the gnutls priority string.

When the set ciphers start with '+', '-' or '!', it is *appended* to the
curl generated priority string. Otherwise it replaces the curl one
completely.

Add test_17_18 to check various combinations.

Closes #16557

.github/scripts/spellcheck.words
docs/CIPHERS.md
docs/libcurl/opts/CURLOPT_SSL_CIPHER_LIST.md
lib/vtls/gtls.c
tests/http/test_17_ssl_use.py

index 7d2efbc54737cc0e954da8234a3c7a1696194104..d0eda7101600ca4ff8b20a01095c18b31df866a0 100644 (file)
@@ -185,6 +185,7 @@ devcpp
 DevOps
 devtools
 DHCP
+DHE
 dir
 distro
 distro's
index ba72326bbebe41d0ddaf281bb37544cca157572c..fb8814302e26e8c40e41246315bc9afa8e990dfd 100644 (file)
@@ -6,6 +6,39 @@ SPDX-License-Identifier: curl
 
 ## curl cipher options
 
+A TLS handshake involves many parameters which take part in the negotiation
+between client and server in order to agree on the TLS version and set of
+algorithms to use for a connection.
+
+What has become known as a "cipher" or better "cipher suite" in TLS
+are names for specific combinations of
+[key exchange](https://en.wikipedia.org/wiki/Key_exchange),
+[bulk encryption](https://en.wikipedia.org/wiki/Link_encryption),
+[message authentication code](https://en.wikipedia.org/wiki/Message_authentication_code)
+and with TLSv1.3 the
+[authenticated encryption](https://en.wikipedia.org/wiki/Authenticated_encryption).
+In addition, there are other parameters that influence the TLS handshake, like
+[DHE](https://en.wikipedia.org/wiki/Diffie–Hellman_key_exchange) "groups" and
+[ECDHE](https://en.wikipedia.org/wiki/Elliptic-curve_Diffie–Hellman) with its
+"curves".
+
+### History
+
+curl's way of letting users configure these settings closely followed OpenSSL
+in its API. TLS learned new parameters, OpenSSL added new API functions and
+curl added command line options.
+
+Several other TLS backends followed the OpenSSL approach, more or less closely,
+and curl maps the command line options to these TLS backends. Some TLS
+backends do not support all of it and command line options are either
+ignored or lead to an error.
+
+Many examples below show the OpenSSL-like use of these options. GnuTLS
+however chose a different approach. These are described in a separate
+section further below.
+
+## ciphers, the OpenSSL way
+
 With curl's option
 [`--tls13-ciphers`](https://curl.se/docs/manpage.html#--tls13-ciphers)
 or
@@ -38,7 +71,7 @@ cipher suites supported by the server and the cipher suites sent by curl. If
 the server is configured to honor the client's cipher preference, the first
 common cipher suite in the list sent by curl is chosen.
 
-## TLS 1.3 cipher suites
+### TLS 1.3 cipher suites
 
 Setting TLS 1.3 cipher suites is supported by curl with
 OpenSSL (1.1.1+, curl 7.61.0+), LibreSSL (3.4.1+, curl 8.3.0+),
@@ -53,14 +86,14 @@ TLS_AES_128_CCM_SHA256
 TLS_AES_128_CCM_8_SHA256
 ```
 
-### wolfSSL notes
+#### wolfSSL notes
 
 In addition to above list the following cipher suites can be used:
 `TLS_SM4_GCM_SM3` `TLS_SM4_CCM_SM3` `TLS_SHA256_SHA256` `TLS_SHA384_SHA384`.
 Usage of these cipher suites is not recommended. (The last two cipher suites
 are NULL ciphers, offering no encryption whatsoever.)
 
-## TLS 1.2 (1.1, 1.0) cipher suites
+### TLS 1.2 (1.1, 1.0) cipher suites
 
 Setting TLS 1.2 cipher suites is supported by curl with OpenSSL, LibreSSL,
 BoringSSL, mbedTLS (curl 8.8.0+), wolfSSL (curl 7.53.0+),
@@ -118,7 +151,7 @@ DES-CBC3-SHA
 See this [list](https://github.com/curl/curl/blob/master/docs/CIPHERS-TLS12.md)
 for a complete list of TLS 1.2 cipher suites.
 
-### OpenSSL notes
+#### OpenSSL notes
 
 In addition to specifying a list of cipher suites, OpenSSL also accepts a
 format with specific cipher strings (like `TLSv1.2`, `AESGCM`, `CHACHA20`) and
@@ -126,7 +159,7 @@ format with specific cipher strings (like `TLSv1.2`, `AESGCM`, `CHACHA20`) and
 [OpenSSL cipher documentation](https://docs.openssl.org/master/man1/openssl-ciphers/#cipher-list-format)
 for further information on that format.
 
-### Schannel notes
+#### Schannel notes
 
 Schannel does not support setting individual TLS 1.2 cipher suites directly.
 It only allows the enabling and disabling of encryption algorithms. These are
@@ -139,7 +172,7 @@ use](https://learn.microsoft.com/en-us/windows/win32/secauthn/cipher-suites-in-s
 to see how that affects the cipher suite selection. When not specifying the
 `--ciphers` and `--tls13-ciphers` options curl passes this flag by default.
 
-## Examples
+### Examples
 
 ```sh
 curl \
@@ -170,6 +203,64 @@ Restrict TLS 1.2 ciphers to `aes128-gcm` and `chacha20`, use default TLS 1.3
 ciphers (if TLS 1.3 is available). Works with OpenSSL, LibreSSL, BoringSSL,
 mbedTLS, wolfSSL, Secure Transport and BearSSL.
 
+## ciphers, the GnuTLS way
+
+With GnuTLS, curl allows configuration of all TLS parameters via option
+[`--ciphers`](https://curl.se/docs/manpage.html#--ciphers)
+or
+[`CURLOPT_SSL_CIPHER_LIST`](https://curl.se/libcurl/c/CURLOPT_SSL_CIPHER_LIST.html)
+only. The option
+[`--tls13-ciphers`](https://curl.se/docs/manpage.html#--tls13-ciphers)
+or
+[`CURLOPT_TLS13_CIPHERS`](https://curl.se/libcurl/c/CURLOPT_TLS13_CIPHERS.html)
+is being ignored.
+
+`--ciphers` is used to set the GnuTLS **priority string** in
+the following way:
+
+* When the set string starts with '+', '-' or '!' it is *appended* to the
+  priority string libcurl itself generates (separated by ':'). This initial
+  priority depends other settings such as CURLOPT_SSLVERSION(3),
+  CURLOPT_TLSAUTH_USERNAME(3) (for SRP) or if HTTP/3 (QUIC)
+  is being negotiated.
+* Otherwise, the set string fully *replaces* the libcurl generated one. While
+  giving full control to the application, the set priority needs to
+  provide for everything the transfer may need to negotiate. Example: if
+  the set priority only allows TLSv1.2, all HTTP/3 attempts fail.
+
+Users may specify via `--ciphers` anything that GnuTLS supports: ciphers,
+key exchange, MAC, compression, TLS versions, signature algorithms, groups,
+elliptic curves, certificate types. In addition, GnuTLS has a variety of
+other keywords that tweak its operations. Applications or a system
+may define new alias names for priority strings that can then be used here.
+
+Since the order of items in priority strings is significant, it makes no
+sense for curl to puzzle other ssl options somehow together. `--ciphers`
+is the single way to change priority.
+
+### Examples
+
+```sh
+curl \
+  --ciphers '-CIPHER_ALL:+AES-128-GCM:+CHACHA20-POLY1305' \
+  https://example.com/
+```
+Restrict ciphers to `aes128-gcm` and `chacha20` in GnuTLS.
+
+```sh
+curl \
+  --ciphers 'NORMAL:-VERS-ALL:+TLS1.3:-AES-256-GCM' \
+  https://example.com/
+```
+Restrict to only TLS 1.3 without the `aes256-gcm` cipher.
+
+```sh
+curl \
+  --ciphers 'NORMAL:-VERS-ALL:+TLS1.2:-CIPHER_ALL:+CAMELLIA-128-GCM' \
+  https://example.com/
+```
+Restrict to only TLS 1.2 with the `CAMELLIA-128-GCM` cipher.
+
 ## Further reading
 - [OpenSSL cipher suite names documentation](https://docs.openssl.org/master/man1/openssl-ciphers/#cipher-suite-names)
 - [wolfSSL cipher support documentation](https://www.wolfssl.com/documentation/manuals/wolfssl/chapter04.html#cipher-support)
@@ -179,3 +270,4 @@ mbedTLS, wolfSSL, Secure Transport and BearSSL.
 - [Secure Transport cipher suite values](https://developer.apple.com/documentation/security/1550981-ssl_cipher_suite_values)
 - [IANA cipher suites list](https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-4)
 - [Wikipedia cipher suite article](https://en.wikipedia.org/wiki/Cipher_suite)
+- [GnuTLS Priority Strings](https://gnutls.org/manual/html_node/Priority-Strings.html)
index 0aae1e86a5de95940c090f1e863cadd3eed34030..b2ef25888e1c26ec8945dc62fe578183b1ebda8d 100644 (file)
@@ -20,6 +20,7 @@ TLS-backend:
   - wolfSSL
   - mbedTLS
   - rustls
+  - GnuTLS
 Added-in: 7.9
 ---
 
@@ -44,7 +45,7 @@ separated by colons.
 
 For setting TLS 1.3 ciphers see CURLOPT_TLS13_CIPHERS(3).
 
-A valid example of a cipher list is:
+A valid example of a cipher list with OpenSSL is:
 ~~~
 "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:"
 "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305"
@@ -53,6 +54,11 @@ A valid example of a cipher list is:
 For Schannel, you can use this option to set algorithms but not specific
 cipher suites. Refer to the ciphers lists document for algorithms.
 
+GnuTLS has the concept of a
+[priority string](https://gnutls.org/manual/html_node/Priority-Strings.html)
+which has its own syntax and keywords. The string set via
+CURLOPT_SSL_CIPHER_LIST(3) directly influences the priority setting.
+
 Find more details about cipher lists on this URL:
 
  https://curl.se/docs/ssl-ciphers.html
index 841a6d1d6550cc755e7d20d751f9aad7b5484a51..d078c4b9183396ffb4f0abd909aff08e57a164e0 100644 (file)
 /* The last #include file should be: */
 #include "memdebug.h"
 
-#define QUIC_PRIORITY \
-  "NORMAL:-VERS-ALL:+VERS-TLS1.3:-CIPHER-ALL:+AES-128-GCM:+AES-256-GCM:" \
-  "+CHACHA20-POLY1305:+AES-128-CCM:-GROUP-ALL:+GROUP-SECP256R1:" \
-  "+GROUP-X25519:+GROUP-SECP384R1:+GROUP-SECP521R1:" \
-  "%DISABLE_TLS13_COMPAT_MODE"
-
 /* Enable GnuTLS debugging by defining GTLSDEBUG */
 /*#define GTLSDEBUG */
 
@@ -319,12 +313,18 @@ static gnutls_x509_crt_fmt_t gnutls_do_file_type(const char *type)
  */
 #define GNUTLS_SRP "+SRP"
 
+#define QUIC_PRIORITY \
+  "NORMAL:-VERS-ALL:+VERS-TLS1.3:-CIPHER-ALL:+AES-128-GCM:+AES-256-GCM:" \
+  "+CHACHA20-POLY1305:+AES-128-CCM:-GROUP-ALL:+GROUP-SECP256R1:" \
+  "+GROUP-X25519:+GROUP-SECP384R1:+GROUP-SECP521R1:" \
+  "%DISABLE_TLS13_COMPAT_MODE"
+
 static CURLcode
 gnutls_set_ssl_version_min_max(struct Curl_easy *data,
                                struct ssl_peer *peer,
                                struct ssl_primary_config *conn_config,
                                const char **prioritylist,
-                               const char *tls13support)
+                               bool tls13support)
 {
   long ssl_version = conn_config->version;
   long ssl_version_max = conn_config->version_max;
@@ -780,6 +780,63 @@ static int gtls_handshake_cb(gnutls_session_t session, unsigned int htype,
   return 0;
 }
 
+static CURLcode gtls_set_priority(struct Curl_cfilter *cf,
+                                 struct Curl_easy *data,
+                                 struct gtls_ctx *gtls,
+                                 const char *priority)
+{
+  struct ssl_primary_config *conn_config = Curl_ssl_cf_get_primary_config(cf);
+  struct dynbuf buf;
+  const char *err = NULL;
+  CURLcode result = CURLE_OK;
+  int rc;
+
+  Curl_dyn_init(&buf, 4096);
+
+#ifdef USE_GNUTLS_SRP
+  if(conn_config->username) {
+    /* Only add SRP to the cipher list if SRP is requested. Otherwise
+     * GnuTLS will disable TLS 1.3 support. */
+    result = Curl_dyn_add(&buf, priority);
+    if(!result)
+      result = Curl_dyn_add(&buf, ":" GNUTLS_SRP);
+    if(result)
+      goto out;
+    priority = Curl_dyn_ptr(&buf);
+  }
+#endif
+
+  if(conn_config->cipher_list) {
+    if((conn_config->cipher_list[0] == '+') ||
+       (conn_config->cipher_list[0] == '-') ||
+       (conn_config->cipher_list[0] == '!')) {
+       /* add it to out own */
+      if(!Curl_dyn_len(&buf)) {  /* not added yet */
+        result = Curl_dyn_add(&buf, priority);
+        if(result)
+          goto out;
+      }
+      result = Curl_dyn_addf(&buf, ":%s", conn_config->cipher_list);
+      if(result)
+        goto out;
+      priority = Curl_dyn_ptr(&buf);
+    }
+    else /* replace our own completely */
+      priority = conn_config->cipher_list;
+  }
+
+  infof(data, "GnuTLS priority: %s", priority);
+  rc = gnutls_priority_set_direct(gtls->session, priority, &err);
+  if(rc != GNUTLS_E_SUCCESS) {
+    failf(data, "Error %d setting GnuTLS priority: %s", rc, err);
+    result = CURLE_SSL_CONNECT_ERROR;
+  }
+
+out:
+  Curl_dyn_free(&buf);
+  return result;
+}
+
 static CURLcode gtls_client_init(struct Curl_cfilter *cf,
                                  struct Curl_easy *data,
                                  struct ssl_peer *peer,
@@ -792,8 +849,7 @@ static CURLcode gtls_client_init(struct Curl_cfilter *cf,
   int rc;
   bool sni = TRUE; /* default is SNI enabled */
   const char *prioritylist;
-  const char *err = NULL;
-  const char *tls13support;
+  bool tls13support;
   CURLcode result;
 
   if(!gtls_inited)
@@ -888,7 +944,7 @@ static CURLcode gtls_client_init(struct Curl_cfilter *cf,
     return CURLE_SSL_CONNECT_ERROR;
 
   /* "In GnuTLS 3.6.5, TLS 1.3 is enabled by default" */
-  tls13support = gnutls_check_version("3.6.5");
+  tls13support = !!gnutls_check_version("3.6.5");
 
   /* Ensure +SRP comes at the *end* of all relevant strings so that it can be
    * removed if a runtime error indicates that SRP is not supported by this
@@ -913,33 +969,9 @@ static CURLcode gtls_client_init(struct Curl_cfilter *cf,
   if(result)
     return result;
 
-#ifdef USE_GNUTLS_SRP
-  /* Only add SRP to the cipher list if SRP is requested. Otherwise
-   * GnuTLS will disable TLS 1.3 support. */
-  if(config->username) {
-    char *prioritysrp = aprintf("%s:" GNUTLS_SRP, prioritylist);
-    if(!prioritysrp)
-      return CURLE_OUT_OF_MEMORY;
-    rc = gnutls_priority_set_direct(gtls->session, prioritysrp, &err);
-    free(prioritysrp);
-
-    if((rc == GNUTLS_E_INVALID_REQUEST) && err) {
-      infof(data, "This GnuTLS does not support SRP");
-    }
-  }
-  else {
-#endif
-    infof(data, "GnuTLS ciphers: %s", prioritylist);
-    rc = gnutls_priority_set_direct(gtls->session, prioritylist, &err);
-#ifdef USE_GNUTLS_SRP
-  }
-#endif
-
-  if(rc != GNUTLS_E_SUCCESS) {
-    failf(data, "Error %d setting GnuTLS cipher list starting with %s",
-          rc, err);
-    return CURLE_SSL_CONNECT_ERROR;
-  }
+  result = gtls_set_priority(cf, data, gtls, prioritylist);
+  if(result)
+    return result;
 
   if(config->clientcert) {
     if(!gtls->shared_creds->trust_setup) {
@@ -2155,6 +2187,7 @@ const struct Curl_ssl Curl_ssl_gnutls = {
   SSLSUPP_CERTINFO |
   SSLSUPP_PINNEDPUBKEY |
   SSLSUPP_HTTPS_PROXY |
+  SSLSUPP_CIPHER_LIST |
   SSLSUPP_CA_CACHE,
 
   sizeof(struct gtls_ssl_backend_data),
index 427f3b88ba48cacef61e7089564879c837a297a2..c8445251ce1d7a5fc8da6bbdb8519e86d01accbc 100644 (file)
@@ -431,6 +431,8 @@ class TestSSLUse:
         proto = 'h3'
         if proto == 'h3' and not env.have_h3():
             pytest.skip("h3 not supported")
+        if env.curl_uses_lib('gnutls'):
+            pytest.skip("gnutls does not ingore --ciphers on TLSv1.3")
         curl = CurlClient(env=env)
         url = f'https://{env.authority_for(env.domain1, proto)}/curltest/sslinfo'
         r = curl.http_get(url=url, alpn_proto=proto, extra_args=[
@@ -446,3 +448,50 @@ class TestSSLUse:
             '--tls13-ciphers', 'NONSENSE', '--tls-max', '1.2'
         ])
         assert r.exit_code == 0, f'{r}'
+
+    @pytest.mark.parametrize("priority, tls_proto, ciphers, success", [
+        ("", "", [], False),
+        ("NONSENSE", "", [], False),
+        ("+NONSENSE", "", [], False),
+        ("NORMAL:-VERS-ALL:+VERS-TLS1.2", "TLSv1.2", ['ECDHE-RSA-CHACHA20-POLY1305'], True),
+        ("-VERS-ALL:+VERS-TLS1.2", "TLSv1.2", ['ECDHE-RSA-CHACHA20-POLY1305'], True),
+        ("NORMAL", "TLSv1.3", ['TLS_CHACHA20_POLY1305_SHA256'], True),
+        ("NORMAL:-VERS-ALL:+VERS-TLS1.3", "TLSv1.3", ['TLS_CHACHA20_POLY1305_SHA256'], True),
+        ("-VERS-ALL:+VERS-TLS1.3", "TLSv1.3", ['TLS_CHACHA20_POLY1305_SHA256'], True),
+        ("!CHACHA20-POLY1305", "TLSv1.3", ['TLS_AES_128_GCM_SHA256'], True),
+        ("-CIPHER-ALL:+CHACHA20-POLY1305", "TLSv1.3", ['TLS_CHACHA20_POLY1305_SHA256'], True),
+        ("-CIPHER-ALL:+AES-256-GCM", "", [], False),
+        ("-CIPHER-ALL:+AES-128-GCM", "TLSv1.3", ['TLS_AES_128_GCM_SHA256'], True),
+        ("SECURE:-CIPHER-ALL:+AES-128-GCM:-VERS-ALL:+VERS-TLS1.2", "TLSv1.2", ['ECDHE-RSA-AES128-GCM-SHA256'], True),
+        ("-MAC-ALL:+SHA256", "", [], False),
+        ("-MAC-ALL:+AEAD", "TLSv1.3", ['TLS_CHACHA20_POLY1305_SHA256'], True),
+        ("-GROUP-ALL:+GROUP-X25519", "TLSv1.3", ['TLS_CHACHA20_POLY1305_SHA256'], True),
+        ("-GROUP-ALL:+GROUP-SECP192R1", "", [], False),
+        ])
+    def test_17_18_gnutls_priority(self, env: Env, httpd, priority, tls_proto, ciphers, success):
+        # to test setting cipher suites, the AES 256 ciphers are disabled in the test server
+        httpd.set_extra_config('base', [
+            'SSLCipherSuite SSL'
+                ' ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256'
+                ':ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305',
+            'SSLCipherSuite TLSv1.3'
+                ' TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256',
+        ])
+        httpd.reload_if_config_changed()
+        proto = 'http/1.1'
+        curl = CurlClient(env=env)
+        url = f'https://{env.authority_for(env.domain1, proto)}/curltest/sslinfo'
+        # SSL backend specifics
+        if not env.curl_uses_lib('gnutls'):
+            pytest.skip('curl not build with GnuTLS')
+        # test
+        extra_args = ['--ciphers', f'{priority}']
+        r = curl.http_get(url=url, alpn_proto=proto, extra_args=extra_args)
+        if success:
+            assert r.exit_code == 0, r.dump_logs()
+            assert r.json['HTTPS'] == 'on', r.dump_logs()
+            if tls_proto:
+                assert r.json['SSL_PROTOCOL'] == tls_proto, r.dump_logs()
+            assert r.json['SSL_CIPHER'] in ciphers, r.dump_logs()
+        else:
+            assert r.exit_code != 0, r.dump_logs()