gnutls: support CA caching

- 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
This commit is contained in:
Stefan Eissing 2024-05-27 16:50:15 +02:00 committed by Daniel Stenberg
parent 4a7bb40dea
commit 5dd8f13bfc
No known key found for this signature in database
GPG Key ID: 5CC908FDB71E12C2
4 changed files with 225 additions and 36 deletions

View 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

View 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;

View 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),

View 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,