]> git.ipfire.org Git - thirdparty/curl.git/commitdiff
gnutls: support CA caching
authorStefan Eissing <stefan@eissing.org>
Mon, 27 May 2024 14:50:15 +0000 (16:50 +0200)
committerDaniel Stenberg <daniel@haxx.se>
Tue, 4 Jun 2024 06:17:55 +0000 (08:17 +0200)
- similar to openssl, use a shared 'credentials' instance
  among TLS connections with a plain configuration.
- different to openssl, a connection with a client certificate
  is not eligible to sharing.
- document CURLOPT_CA_CACHE_TIMEOUT in man page

Closes #13795

docs/libcurl/opts/CURLOPT_CA_CACHE_TIMEOUT.md
lib/vquic/vquic-tls.c
lib/vtls/gtls.c
lib/vtls/gtls.h

index e9ad726446f874922293faa85113427336797aa2..78b5ae78a0b73148c74d7a314ce10a0f67dd8d4d 100644 (file)
@@ -13,6 +13,7 @@ See-also:
 Protocol:
   - TLS
 TLS-backend:
+  - GnuTLS
   - OpenSSL
   - Schannel
   - wolfSSL
@@ -78,7 +79,7 @@ int main(void)
 This option was added in curl 7.87.0.
 
 This option is supported by OpenSSL and its forks (since 7.87.0), Schannel
-(since 8.5.0) and wolfSSL (since 8.9.0).
+(since 8.5.0), wolfSSL (since 8.9.0) and GnuTLS (since 8.9.0).
 
 # RETURN VALUE
 
index 715e36dfb9d13e3e1b5ad1576225240fc4c2bddc..44584182a263f8a5c61b205187d59dc87f56a7a0 100644 (file)
@@ -262,10 +262,9 @@ void Curl_vquic_tls_cleanup(struct curl_tls_ctx *ctx)
   if(ctx->ossl.ssl_ctx)
     SSL_CTX_free(ctx->ossl.ssl_ctx);
 #elif defined(USE_GNUTLS)
-  if(ctx->gtls.cred)
-    gnutls_certificate_free_credentials(ctx->gtls.cred);
   if(ctx->gtls.session)
     gnutls_deinit(ctx->gtls.session);
+  Curl_gtls_shared_creds_free(&ctx->gtls.shared_creds);
 #elif defined(USE_WOLFSSL)
   if(ctx->wssl.handle)
     wolfSSL_free(ctx->wssl.handle);
@@ -293,7 +292,7 @@ CURLcode Curl_vquic_tls_before_recv(struct curl_tls_ctx *ctx,
       return result;
   }
 #elif defined(USE_GNUTLS)
-  if(!ctx->gtls.trust_setup) {
+  if(!ctx->gtls.shared_creds->trust_setup) {
     CURLcode result = Curl_gtls_client_trust_setup(cf, data, &ctx->gtls);
     if(result)
       return result;
index 1ae384d39eb13688eab454adad09626978d4e6f3..9038739d4ecaea631b9d5428f0b34253e6c451c0 100644 (file)
@@ -125,7 +125,7 @@ static ssize_t gtls_pull(void *s, void *buf, size_t blen)
   CURLcode result;
 
   DEBUGASSERT(data);
-  if(!backend->gtls.trust_setup) {
+  if(!backend->gtls.shared_creds->trust_setup) {
     result = Curl_gtls_client_trust_setup(cf, data, &backend->gtls);
     if(result) {
       gnutls_transport_set_errno(backend->gtls.session, EINVAL);
@@ -297,7 +297,7 @@ static CURLcode handshake(struct Curl_cfilter *cf,
     backend->gtls.io_result = CURLE_OK;
     rc = gnutls_handshake(session);
 
-    if(!backend->gtls.trust_setup) {
+    if(!backend->gtls.shared_creds->trust_setup) {
       /* After having send off the ClientHello, we prepare the trust
        * store to verify the coming certificate from the server */
       CURLcode result = Curl_gtls_client_trust_setup(cf, data, &backend->gtls);
@@ -451,20 +451,67 @@ set_ssl_version_min_max(struct Curl_easy *data,
   return CURLE_SSL_CONNECT_ERROR;
 }
 
-CURLcode Curl_gtls_client_trust_setup(struct Curl_cfilter *cf,
-                                      struct Curl_easy *data,
-                                      struct gtls_ctx *gtls)
+CURLcode Curl_gtls_shared_creds_create(struct Curl_easy *data,
+                                       struct gtls_shared_creds **pcreds)
+{
+  struct gtls_shared_creds *shared;
+  int rc;
+
+  *pcreds = NULL;
+  shared = calloc(1, sizeof(*shared));
+  if(!shared)
+    return CURLE_OUT_OF_MEMORY;
+
+  rc = gnutls_certificate_allocate_credentials(&shared->creds);
+  if(rc != GNUTLS_E_SUCCESS) {
+    failf(data, "gnutls_cert_all_cred() failed: %s", gnutls_strerror(rc));
+    free(shared);
+    return CURLE_SSL_CONNECT_ERROR;
+  }
+
+  shared->refcount = 1;
+  shared->time = Curl_now();
+  *pcreds = shared;
+  return CURLE_OK;
+}
+
+CURLcode Curl_gtls_shared_creds_up_ref(struct gtls_shared_creds *creds)
+{
+  DEBUGASSERT(creds);
+  if(creds->refcount < SIZE_T_MAX) {
+    ++creds->refcount;
+    return CURLE_OK;
+  }
+  return CURLE_BAD_FUNCTION_ARGUMENT;
+}
+
+void Curl_gtls_shared_creds_free(struct gtls_shared_creds **pcreds)
+{
+  struct gtls_shared_creds *shared = *pcreds;
+  *pcreds = NULL;
+  if(shared) {
+    --shared->refcount;
+    if(!shared->refcount) {
+      gnutls_certificate_free_credentials(shared->creds);
+      free(shared->CAfile);
+      free(shared);
+    }
+  }
+}
+
+static CURLcode gtls_populate_creds(struct Curl_cfilter *cf,
+                                    struct Curl_easy *data,
+                                    gnutls_certificate_credentials_t creds)
 {
   struct ssl_primary_config *config = Curl_ssl_cf_get_primary_config(cf);
   struct ssl_config_data *ssl_config = Curl_ssl_cf_get_config(cf, data);
   int rc;
 
-  CURL_TRC_CF(data, cf, "setup trust anchors and CRLs");
   if(config->verifypeer) {
     bool imported_native_ca = false;
 
     if(ssl_config->native_ca_store) {
-      rc = gnutls_certificate_set_x509_system_trust(gtls->cred);
+      rc = gnutls_certificate_set_x509_system_trust(creds);
       if(rc < 0)
         infof(data, "error reading native ca store (%s), continuing anyway",
               gnutls_strerror(rc));
@@ -477,10 +524,10 @@ CURLcode Curl_gtls_client_trust_setup(struct Curl_cfilter *cf,
 
     if(config->CAfile) {
       /* set the trusted CA cert bundle file */
-      gnutls_certificate_set_verify_flags(gtls->cred,
+      gnutls_certificate_set_verify_flags(creds,
                                           GNUTLS_VERIFY_ALLOW_X509_V1_CA_CRT);
 
-      rc = gnutls_certificate_set_x509_trust_file(gtls->cred,
+      rc = gnutls_certificate_set_x509_trust_file(creds,
                                                   config->CAfile,
                                                   GNUTLS_X509_FMT_PEM);
       if(rc < 0) {
@@ -498,8 +545,7 @@ CURLcode Curl_gtls_client_trust_setup(struct Curl_cfilter *cf,
 
     if(config->CApath) {
       /* set the trusted CA cert directory */
-      rc = gnutls_certificate_set_x509_trust_dir(gtls->cred,
-                                                 config->CApath,
+      rc = gnutls_certificate_set_x509_trust_dir(creds, config->CApath,
                                                  GNUTLS_X509_FMT_PEM);
       if(rc < 0) {
         infof(data, "error reading ca cert file %s (%s)%s",
@@ -517,8 +563,7 @@ CURLcode Curl_gtls_client_trust_setup(struct Curl_cfilter *cf,
 
   if(config->CRLfile) {
     /* set the CRL list file */
-    rc = gnutls_certificate_set_x509_crl_file(gtls->cred,
-                                              config->CRLfile,
+    rc = gnutls_certificate_set_x509_crl_file(creds, config->CRLfile,
                                               GNUTLS_X509_FMT_PEM);
     if(rc < 0) {
       failf(data, "error reading crl file %s (%s)",
@@ -529,7 +574,141 @@ CURLcode Curl_gtls_client_trust_setup(struct Curl_cfilter *cf,
       infof(data, "found %d CRL in %s", rc, config->CRLfile);
   }
 
-  gtls->trust_setup = TRUE;
+  return CURLE_OK;
+}
+
+/* key to use at `multi->proto_hash` */
+#define MPROTO_GTLS_X509_KEY   "tls:gtls:x509:share"
+
+static bool gtls_shared_creds_expired(const struct Curl_easy *data,
+                                      const struct gtls_shared_creds *sc)
+{
+  const struct ssl_general_config *cfg = &data->set.general_ssl;
+  struct curltime now = Curl_now();
+  timediff_t elapsed_ms = Curl_timediff(now, sc->time);
+  timediff_t timeout_ms = cfg->ca_cache_timeout * (timediff_t)1000;
+
+  if(timeout_ms < 0)
+    return false;
+
+  return elapsed_ms >= timeout_ms;
+}
+
+static bool gtls_shared_creds_different(struct Curl_cfilter *cf,
+                                        const struct gtls_shared_creds *sc)
+{
+  struct ssl_primary_config *conn_config = Curl_ssl_cf_get_primary_config(cf);
+  if(!sc->CAfile || !conn_config->CAfile)
+    return sc->CAfile != conn_config->CAfile;
+
+  return strcmp(sc->CAfile, conn_config->CAfile);
+}
+
+static struct gtls_shared_creds*
+gtls_get_cached_creds(struct Curl_cfilter *cf, struct Curl_easy *data)
+{
+  struct gtls_shared_creds *shared_creds;
+
+  if(data->multi) {
+    shared_creds = Curl_hash_pick(&data->multi->proto_hash,
+                                  (void *)MPROTO_GTLS_X509_KEY,
+                                  sizeof(MPROTO_GTLS_X509_KEY)-1);
+     if(shared_creds && shared_creds->creds &&
+        !gtls_shared_creds_expired(data, shared_creds) &&
+        !gtls_shared_creds_different(cf, shared_creds)) {
+       return shared_creds;
+     }
+  }
+  return NULL;
+}
+
+static void gtls_shared_creds_hash_free(void *key, size_t key_len, void *p)
+{
+  struct gtls_shared_creds *sc = p;
+  DEBUGASSERT(key_len == (sizeof(MPROTO_GTLS_X509_KEY)-1));
+  DEBUGASSERT(!memcmp(MPROTO_GTLS_X509_KEY, key, key_len));
+  (void)key;
+  (void)key_len;
+  Curl_gtls_shared_creds_free(&sc); /* down reference */
+}
+
+static void gtls_set_cached_creds(struct Curl_cfilter *cf,
+                                  struct Curl_easy *data,
+                                  struct gtls_shared_creds *sc)
+{
+  struct ssl_primary_config *conn_config = Curl_ssl_cf_get_primary_config(cf);
+
+  DEBUGASSERT(sc);
+  DEBUGASSERT(sc->creds);
+  DEBUGASSERT(!sc->CAfile);
+  DEBUGASSERT(sc->refcount == 1);
+  if(!data->multi)
+    return;
+
+  if(conn_config->CAfile) {
+    sc->CAfile = strdup(conn_config->CAfile);
+    if(!sc->CAfile)
+      return;
+  }
+
+  if(Curl_gtls_shared_creds_up_ref(sc))
+    return;
+
+  if(!Curl_hash_add2(&data->multi->proto_hash,
+                    (void *)MPROTO_GTLS_X509_KEY,
+                    sizeof(MPROTO_GTLS_X509_KEY)-1,
+                    sc, gtls_shared_creds_hash_free)) {
+    Curl_gtls_shared_creds_free(&sc); /* down reference again */
+    return;
+  }
+}
+
+CURLcode Curl_gtls_client_trust_setup(struct Curl_cfilter *cf,
+                                      struct Curl_easy *data,
+                                      struct gtls_ctx *gtls)
+{
+  struct ssl_primary_config *conn_config = Curl_ssl_cf_get_primary_config(cf);
+  struct ssl_config_data *ssl_config = Curl_ssl_cf_get_config(cf, data);
+  struct gtls_shared_creds *cached_creds = NULL;
+  bool cache_criteria_met;
+  CURLcode result;
+  int rc;
+
+
+  /* Consider the X509 store cacheable if it comes exclusively from a CAfile,
+    or no source is provided and we are falling back to openssl's built-in
+    default. */
+  cache_criteria_met = (data->set.general_ssl.ca_cache_timeout != 0) &&
+    conn_config->verifypeer &&
+    !conn_config->CApath &&
+    !conn_config->ca_info_blob &&
+    !ssl_config->primary.CRLfile &&
+    !ssl_config->native_ca_store &&
+    !conn_config->clientcert; /* GNUTls adds client cert to its credentials! */
+
+  if(cache_criteria_met)
+    cached_creds = gtls_get_cached_creds(cf, data);
+
+  if(cached_creds && !Curl_gtls_shared_creds_up_ref(cached_creds)) {
+    CURL_TRC_CF(data, cf, "using shared trust anchors and CRLs");
+    Curl_gtls_shared_creds_free(&gtls->shared_creds);
+    gtls->shared_creds = cached_creds;
+    rc = gnutls_credentials_set(gtls->session, GNUTLS_CRD_CERTIFICATE,
+                                gtls->shared_creds->creds);
+    if(rc != GNUTLS_E_SUCCESS) {
+      failf(data, "gnutls_credentials_set() failed: %s", gnutls_strerror(rc));
+      return CURLE_SSL_CONNECT_ERROR;
+    }
+  }
+  else {
+    CURL_TRC_CF(data, cf, "loading trust anchors and CRLs");
+    result = gtls_populate_creds(cf, data, gtls->shared_creds->creds);
+    if(result)
+      return result;
+    gtls->shared_creds->trust_setup = TRUE;
+    if(cache_criteria_met)
+      gtls_set_cached_creds(cf, data, gtls->shared_creds);
+  }
   return CURLE_OK;
 }
 
@@ -640,12 +819,10 @@ static CURLcode gtls_client_init(struct Curl_cfilter *cf,
   else if(config->version == CURL_SSLVERSION_SSLv3)
     sni = FALSE; /* SSLv3 has no SNI */
 
-  /* allocate a cred struct */
-  rc = gnutls_certificate_allocate_credentials(&gtls->cred);
-  if(rc != GNUTLS_E_SUCCESS) {
-    failf(data, "gnutls_cert_all_cred() failed: %s", gnutls_strerror(rc));
-    return CURLE_SSL_CONNECT_ERROR;
-  }
+  /* allocate a shared creds struct */
+  result = Curl_gtls_shared_creds_create(data, &gtls->shared_creds);
+  if(result)
+    return result;
 
 #ifdef USE_GNUTLS_SRP
   if(config->username && Curl_auth_allowed_to_host(data)) {
@@ -757,7 +934,7 @@ static CURLcode gtls_client_init(struct Curl_cfilter *cf,
   }
 
   if(config->clientcert) {
-    if(!gtls->trust_setup) {
+    if(!gtls->shared_creds->trust_setup) {
       result = Curl_gtls_client_trust_setup(cf, data, gtls);
       if(result)
         return result;
@@ -769,7 +946,7 @@ static CURLcode gtls_client_init(struct Curl_cfilter *cf,
         GNUTLS_PKCS_USE_PBES2_AES_128 | GNUTLS_PKCS_USE_PBES2_AES_192 |
         GNUTLS_PKCS_USE_PBES2_AES_256;
       rc = gnutls_certificate_set_x509_key_file2(
-           gtls->cred,
+           gtls->shared_creds->creds,
            config->clientcert,
            ssl_config->key ? ssl_config->key : config->clientcert,
            do_file_type(ssl_config->cert_type),
@@ -784,7 +961,7 @@ static CURLcode gtls_client_init(struct Curl_cfilter *cf,
     }
     else {
       if(gnutls_certificate_set_x509_key_file(
-           gtls->cred,
+           gtls->shared_creds->creds,
            config->clientcert,
            ssl_config->key ? ssl_config->key : config->clientcert,
            do_file_type(ssl_config->cert_type) ) !=
@@ -809,7 +986,7 @@ static CURLcode gtls_client_init(struct Curl_cfilter *cf,
 #endif
   {
     rc = gnutls_credentials_set(gtls->session, GNUTLS_CRD_CERTIFICATE,
-                                gtls->cred);
+                                gtls->shared_creds->creds);
     if(rc != GNUTLS_E_SUCCESS) {
       failf(data, "gnutls_credentials_set() failed: %s", gnutls_strerror(rc));
       return CURLE_SSL_CONNECT_ERROR;
@@ -1645,9 +1822,8 @@ static void gtls_close(struct Curl_cfilter *cf,
     gnutls_deinit(backend->gtls.session);
     backend->gtls.session = NULL;
   }
-  if(backend->gtls.cred) {
-    gnutls_certificate_free_credentials(backend->gtls.cred);
-    backend->gtls.cred = NULL;
+  if(backend->gtls.shared_creds) {
+    Curl_gtls_shared_creds_free(&backend->gtls.shared_creds);
   }
 #ifdef USE_GNUTLS_SRP
   if(backend->gtls.srp_client_cred) {
@@ -1724,7 +1900,6 @@ static int gtls_shutdown(struct Curl_cfilter *cf,
     }
     gnutls_deinit(backend->gtls.session);
   }
-  gnutls_certificate_free_credentials(backend->gtls.cred);
 
 #ifdef USE_GNUTLS_SRP
   {
@@ -1734,8 +1909,8 @@ static int gtls_shutdown(struct Curl_cfilter *cf,
   }
 #endif
 
-  backend->gtls.cred = NULL;
   backend->gtls.session = NULL;
+  Curl_gtls_shared_creds_free(&backend->gtls.shared_creds);
 
   return retval;
 }
@@ -1837,7 +2012,8 @@ const struct Curl_ssl Curl_ssl_gnutls = {
   SSLSUPP_CA_PATH  |
   SSLSUPP_CERTINFO |
   SSLSUPP_PINNEDPUBKEY |
-  SSLSUPP_HTTPS_PROXY,
+  SSLSUPP_HTTPS_PROXY |
+  SSLSUPP_CA_CACHE,
 
   sizeof(struct gtls_ssl_backend_data),
 
index f8388b37b8ef76130ea13409885cbcc4db2bb6d1..73dea8fa89f6321adcc0a4a50a1e856e9e45d4bd 100644 (file)
@@ -30,6 +30,7 @@
 #ifdef USE_GNUTLS
 
 #include <gnutls/gnutls.h>
+#include "timeval.h"
 
 #ifdef HAVE_GNUTLS_SRP
 /* the function exists */
@@ -45,14 +46,26 @@ struct ssl_primary_config;
 struct ssl_config_data;
 struct ssl_peer;
 
+struct gtls_shared_creds {
+  gnutls_certificate_credentials_t creds;
+  char *CAfile; /* CAfile path used to generate X509 store */
+  struct curltime time; /* when the shared creds was created */
+  size_t refcount;
+  BIT(trust_setup); /* x509 anchors + CRLs have been set up */
+};
+
+CURLcode Curl_gtls_shared_creds_create(struct Curl_easy *data,
+                                       struct gtls_shared_creds **pcreds);
+CURLcode Curl_gtls_shared_creds_up_ref(struct gtls_shared_creds *creds);
+void Curl_gtls_shared_creds_free(struct gtls_shared_creds **pcreds);
+
 struct gtls_ctx {
   gnutls_session_t session;
-  gnutls_certificate_credentials_t cred;
+  struct gtls_shared_creds *shared_creds;
 #ifdef USE_GNUTLS_SRP
   gnutls_srp_client_credentials_t srp_client_cred;
 #endif
   CURLcode io_result; /* result of last IO cfilter operation */
-  BIT(trust_setup); /* x509 anchors + CRLs have been set up */
 };
 
 typedef CURLcode Curl_gtls_ctx_setup_cb(struct Curl_cfilter *cf,