s2-mod/deps/curl/lib/vtls/vtls_scache.c
2025-04-28 23:30:04 -04:00

1241 lines
35 KiB
C

/***************************************************************************
* _ _ ____ _
* Project ___| | | | _ \| |
* / __| | | | |_) | |
* | (__| |_| | _ <| |___
* \___|\___/|_| \_\_____|
*
* Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
*
* This software is licensed as described in the file COPYING, which
* you should have received as part of this distribution. The terms
* are also available at https://curl.se/docs/copyright.html.
*
* You may opt to use, copy, modify, merge, publish, distribute and/or sell
* copies of the Software, and permit persons to whom the Software is
* furnished to do so, under the terms of the COPYING file.
*
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
* KIND, either express or implied.
*
* SPDX-License-Identifier: curl
*
***************************************************************************/
#include "curl_setup.h"
#ifdef USE_SSL
#ifdef HAVE_SYS_TYPES_H
#include <sys/types.h>
#endif
#ifdef HAVE_SYS_STAT_H
#include <sys/stat.h>
#endif
#ifdef HAVE_FCNTL_H
#include <fcntl.h>
#endif
#include "urldata.h"
#include "cfilters.h"
#include "vtls.h" /* generic SSL protos etc */
#include "vtls_int.h"
#include "vtls_scache.h"
#include "vtls_spack.h"
#include "strcase.h"
#include "url.h"
#include "llist.h"
#include "share.h"
#include "curl_trc.h"
#include "curl_sha256.h"
#include "rand.h"
#include "warnless.h"
#include "curl_printf.h"
#include "strdup.h"
/* The last #include files should be: */
#include "curl_memory.h"
#include "memdebug.h"
static bool cf_ssl_peer_key_is_global(const char *peer_key);
/* a peer+tls-config we cache sessions for */
struct Curl_ssl_scache_peer {
char *ssl_peer_key; /* id for peer + relevant TLS configuration */
char *clientcert;
char *srp_username;
char *srp_password;
struct Curl_llist sessions;
void *sobj; /* object instance or NULL */
Curl_ssl_scache_obj_dtor *sobj_free; /* free `sobj` callback */
unsigned char key_salt[CURL_SHA256_DIGEST_LENGTH]; /* for entry export */
unsigned char key_hmac[CURL_SHA256_DIGEST_LENGTH]; /* for entry export */
size_t max_sessions;
long age; /* just a number, the higher the more recent */
BIT(hmac_set); /* if key_salt and key_hmac are present */
BIT(exportable); /* sessions for this peer can be exported */
};
#define CURL_SCACHE_MAGIC 0x000e1551
#define GOOD_SCACHE(x) ((x) && (x)->magic == CURL_SCACHE_MAGIC)
struct Curl_ssl_scache {
unsigned int magic;
struct Curl_ssl_scache_peer *peers;
size_t peer_count;
int default_lifetime_secs;
long age;
};
static struct Curl_ssl_scache *cf_ssl_scache_get(struct Curl_easy *data)
{
struct Curl_ssl_scache *scache = NULL;
/* If a share is present, its ssl_scache has preference over the multi */
if(data->share && data->share->ssl_scache)
scache = data->share->ssl_scache;
else if(data->multi && data->multi->ssl_scache)
scache = data->multi->ssl_scache;
if(scache && !GOOD_SCACHE(scache)) {
failf(data, "transfer would use an invalid scache at %p, denied",
(void *)scache);
DEBUGASSERT(0);
return NULL;
}
return scache;
}
static void cf_ssl_scache_sesssion_ldestroy(void *udata, void *obj)
{
struct Curl_ssl_session *s = obj;
(void)udata;
free(CURL_UNCONST(s->sdata));
free(CURL_UNCONST(s->quic_tp));
free((void *)s->alpn);
free(s);
}
CURLcode
Curl_ssl_session_create(void *sdata, size_t sdata_len,
int ietf_tls_id, const char *alpn,
curl_off_t valid_until, size_t earlydata_max,
struct Curl_ssl_session **psession)
{
return Curl_ssl_session_create2(sdata, sdata_len, ietf_tls_id, alpn,
valid_until, earlydata_max,
NULL, 0, psession);
}
CURLcode
Curl_ssl_session_create2(void *sdata, size_t sdata_len,
int ietf_tls_id, const char *alpn,
curl_off_t valid_until, size_t earlydata_max,
unsigned char *quic_tp, size_t quic_tp_len,
struct Curl_ssl_session **psession)
{
struct Curl_ssl_session *s;
if(!sdata || !sdata_len) {
free(sdata);
return CURLE_BAD_FUNCTION_ARGUMENT;
}
*psession = NULL;
s = calloc(1, sizeof(*s));
if(!s) {
free(sdata);
free(quic_tp);
return CURLE_OUT_OF_MEMORY;
}
s->ietf_tls_id = ietf_tls_id;
s->valid_until = valid_until;
s->earlydata_max = earlydata_max;
s->sdata = sdata;
s->sdata_len = sdata_len;
s->quic_tp = quic_tp;
s->quic_tp_len = quic_tp_len;
if(alpn) {
s->alpn = strdup(alpn);
if(!s->alpn) {
cf_ssl_scache_sesssion_ldestroy(NULL, s);
return CURLE_OUT_OF_MEMORY;
}
}
*psession = s;
return CURLE_OK;
}
void Curl_ssl_session_destroy(struct Curl_ssl_session *s)
{
if(s) {
/* if in the list, the list destructor takes care of it */
if(Curl_node_llist(&s->list))
Curl_node_remove(&s->list);
else {
cf_ssl_scache_sesssion_ldestroy(NULL, s);
}
}
}
static void cf_ssl_scache_clear_peer(struct Curl_ssl_scache_peer *peer)
{
Curl_llist_destroy(&peer->sessions, NULL);
if(peer->sobj) {
DEBUGASSERT(peer->sobj_free);
if(peer->sobj_free)
peer->sobj_free(peer->sobj);
peer->sobj = NULL;
}
peer->sobj_free = NULL;
Curl_safefree(peer->clientcert);
#ifdef USE_TLS_SRP
Curl_safefree(peer->srp_username);
Curl_safefree(peer->srp_password);
#endif
Curl_safefree(peer->ssl_peer_key);
peer->age = 0;
peer->hmac_set = FALSE;
}
static void cf_ssl_scache_peer_set_obj(struct Curl_ssl_scache_peer *peer,
void *sobj,
Curl_ssl_scache_obj_dtor *sobj_free)
{
DEBUGASSERT(peer);
if(peer->sobj_free) {
peer->sobj_free(peer->sobj);
}
peer->sobj = sobj;
peer->sobj_free = sobj_free;
}
static void cf_ssl_cache_peer_update(struct Curl_ssl_scache_peer *peer)
{
/* The sessions of this peer are exportable if
* - it has no confidential information
* - its peer key is not yet known, because sessions were
* imported using only the salt+hmac
* - the peer key is global, e.g. carrying no relative paths */
peer->exportable = (!peer->clientcert && !peer->srp_username &&
!peer->srp_password &&
(!peer->ssl_peer_key ||
cf_ssl_peer_key_is_global(peer->ssl_peer_key)));
}
static CURLcode
cf_ssl_scache_peer_init(struct Curl_ssl_scache_peer *peer,
const char *ssl_peer_key,
const char *clientcert,
const char *srp_username,
const char *srp_password,
const unsigned char *salt,
const unsigned char *hmac)
{
CURLcode result = CURLE_OUT_OF_MEMORY;
DEBUGASSERT(!peer->ssl_peer_key);
if(ssl_peer_key) {
peer->ssl_peer_key = strdup(ssl_peer_key);
if(!peer->ssl_peer_key)
goto out;
peer->hmac_set = FALSE;
}
else if(salt && hmac) {
memcpy(peer->key_salt, salt, sizeof(peer->key_salt));
memcpy(peer->key_hmac, hmac, sizeof(peer->key_hmac));
peer->hmac_set = TRUE;
}
else {
result = CURLE_BAD_FUNCTION_ARGUMENT;
goto out;
}
if(clientcert) {
peer->clientcert = strdup(clientcert);
if(!peer->clientcert)
goto out;
}
if(srp_username) {
peer->srp_username = strdup(srp_username);
if(!peer->srp_username)
goto out;
}
if(srp_password) {
peer->srp_password = strdup(srp_password);
if(!peer->srp_password)
goto out;
}
cf_ssl_cache_peer_update(peer);
result = CURLE_OK;
out:
if(result)
cf_ssl_scache_clear_peer(peer);
return result;
}
static void cf_scache_session_remove(struct Curl_ssl_scache_peer *peer,
struct Curl_ssl_session *s)
{
(void)peer;
DEBUGASSERT(Curl_node_llist(&s->list) == &peer->sessions);
Curl_ssl_session_destroy(s);
}
static bool cf_scache_session_expired(struct Curl_ssl_session *s,
curl_off_t now)
{
return (s->valid_until > 0) && (s->valid_until < now);
}
static void cf_scache_peer_remove_expired(struct Curl_ssl_scache_peer *peer,
curl_off_t now)
{
struct Curl_llist_node *n = Curl_llist_head(&peer->sessions);
while(n) {
struct Curl_ssl_session *s = Curl_node_elem(n);
n = Curl_node_next(n);
if(cf_scache_session_expired(s, now))
cf_scache_session_remove(peer, s);
}
}
static void cf_scache_peer_remove_non13(struct Curl_ssl_scache_peer *peer)
{
struct Curl_llist_node *n = Curl_llist_head(&peer->sessions);
while(n) {
struct Curl_ssl_session *s = Curl_node_elem(n);
n = Curl_node_next(n);
if(s->ietf_tls_id != CURL_IETF_PROTO_TLS1_3)
cf_scache_session_remove(peer, s);
}
}
CURLcode Curl_ssl_scache_create(size_t max_peers,
size_t max_sessions_per_peer,
struct Curl_ssl_scache **pscache)
{
struct Curl_ssl_scache *scache;
struct Curl_ssl_scache_peer *peers;
size_t i;
*pscache = NULL;
peers = calloc(max_peers, sizeof(*peers));
if(!peers)
return CURLE_OUT_OF_MEMORY;
scache = calloc(1, sizeof(*scache));
if(!scache) {
free(peers);
return CURLE_OUT_OF_MEMORY;
}
scache->magic = CURL_SCACHE_MAGIC;
scache->default_lifetime_secs = (24*60*60); /* 1 day */
scache->peer_count = max_peers;
scache->peers = peers;
scache->age = 1;
for(i = 0; i < scache->peer_count; ++i) {
scache->peers[i].max_sessions = max_sessions_per_peer;
Curl_llist_init(&scache->peers[i].sessions,
cf_ssl_scache_sesssion_ldestroy);
}
*pscache = scache;
return CURLE_OK;
}
void Curl_ssl_scache_destroy(struct Curl_ssl_scache *scache)
{
if(scache && GOOD_SCACHE(scache)) {
size_t i;
scache->magic = 0;
for(i = 0; i < scache->peer_count; ++i) {
cf_ssl_scache_clear_peer(&scache->peers[i]);
}
free(scache->peers);
free(scache);
}
}
/* Lock shared SSL session data */
void Curl_ssl_scache_lock(struct Curl_easy *data)
{
if(CURL_SHARE_ssl_scache(data))
Curl_share_lock(data, CURL_LOCK_DATA_SSL_SESSION, CURL_LOCK_ACCESS_SINGLE);
}
/* Unlock shared SSL session data */
void Curl_ssl_scache_unlock(struct Curl_easy *data)
{
if(CURL_SHARE_ssl_scache(data))
Curl_share_unlock(data, CURL_LOCK_DATA_SSL_SESSION);
}
static CURLcode cf_ssl_peer_key_add_path(struct dynbuf *buf,
const char *name,
char *path,
bool *is_local)
{
if(path && path[0]) {
/* We try to add absolute paths, so that the session key can stay
* valid when used in another process with different CWD. However,
* when a path does not exist, this does not work. Then, we add
* the path as is. */
#ifdef UNDER_CE
(void)is_local;
return Curl_dyn_addf(buf, ":%s-%s", name, path);
#elif defined(_WIN32)
char abspath[_MAX_PATH];
if(_fullpath(abspath, path, _MAX_PATH))
return Curl_dyn_addf(buf, ":%s-%s", name, abspath);
*is_local = TRUE;
#elif defined(HAVE_REALPATH)
if(path[0] != '/') {
char *abspath = realpath(path, NULL);
if(abspath) {
CURLcode r = Curl_dyn_addf(buf, ":%s-%s", name, abspath);
(free)(abspath); /* allocated by libc, free without memdebug */
return r;
}
*is_local = TRUE;
}
#endif
return Curl_dyn_addf(buf, ":%s-%s", name, path);
}
return CURLE_OK;
}
static CURLcode cf_ssl_peer_key_add_hash(struct dynbuf *buf,
const char *name,
struct curl_blob *blob)
{
CURLcode r = CURLE_OK;
if(blob && blob->len) {
unsigned char hash[CURL_SHA256_DIGEST_LENGTH];
size_t i;
r = Curl_dyn_addf(buf, ":%s-", name);
if(r)
goto out;
r = Curl_sha256it(hash, blob->data, blob->len);
if(r)
goto out;
for(i = 0; i < CURL_SHA256_DIGEST_LENGTH; ++i) {
r = Curl_dyn_addf(buf, "%02x", hash[i]);
if(r)
goto out;
}
}
out:
return r;
}
#define CURL_SSLS_LOCAL_SUFFIX ":L"
#define CURL_SSLS_GLOBAL_SUFFIX ":G"
static bool cf_ssl_peer_key_is_global(const char *peer_key)
{
size_t len = peer_key ? strlen(peer_key) : 0;
return (len > 2) &&
(peer_key[len - 1] == 'G') &&
(peer_key[len - 2] == ':');
}
CURLcode Curl_ssl_peer_key_make(struct Curl_cfilter *cf,
const struct ssl_peer *peer,
const char *tls_id,
char **ppeer_key)
{
struct ssl_primary_config *ssl = Curl_ssl_cf_get_primary_config(cf);
struct dynbuf buf;
size_t key_len;
bool is_local = FALSE;
CURLcode r;
*ppeer_key = NULL;
Curl_dyn_init(&buf, 10 * 1024);
r = Curl_dyn_addf(&buf, "%s:%d", peer->hostname, peer->port);
if(r)
goto out;
switch(peer->transport) {
case TRNSPRT_TCP:
break;
case TRNSPRT_UDP:
r = Curl_dyn_add(&buf, ":UDP");
break;
case TRNSPRT_QUIC:
r = Curl_dyn_add(&buf, ":QUIC");
break;
case TRNSPRT_UNIX:
r = Curl_dyn_add(&buf, ":UNIX");
break;
default:
r = Curl_dyn_addf(&buf, ":TRNSPRT-%d", peer->transport);
break;
}
if(r)
goto out;
if(!ssl->verifypeer) {
r = Curl_dyn_add(&buf, ":NO-VRFY-PEER");
if(r)
goto out;
}
if(!ssl->verifyhost) {
r = Curl_dyn_add(&buf, ":NO-VRFY-HOST");
if(r)
goto out;
}
if(ssl->verifystatus) {
r = Curl_dyn_add(&buf, ":VRFY-STATUS");
if(r)
goto out;
}
if(!ssl->verifypeer || !ssl->verifyhost) {
if(cf->conn->bits.conn_to_host) {
r = Curl_dyn_addf(&buf, ":CHOST-%s", cf->conn->conn_to_host.name);
if(r)
goto out;
}
if(cf->conn->bits.conn_to_port) {
r = Curl_dyn_addf(&buf, ":CPORT-%d", cf->conn->conn_to_port);
if(r)
goto out;
}
}
if(ssl->version || ssl->version_max) {
r = Curl_dyn_addf(&buf, ":TLSVER-%d-%d", ssl->version,
(ssl->version_max >> 16));
if(r)
goto out;
}
if(ssl->ssl_options) {
r = Curl_dyn_addf(&buf, ":TLSOPT-%x", ssl->ssl_options);
if(r)
goto out;
}
if(ssl->cipher_list) {
r = Curl_dyn_addf(&buf, ":CIPHER-%s", ssl->cipher_list);
if(r)
goto out;
}
if(ssl->cipher_list13) {
r = Curl_dyn_addf(&buf, ":CIPHER13-%s", ssl->cipher_list13);
if(r)
goto out;
}
if(ssl->curves) {
r = Curl_dyn_addf(&buf, ":CURVES-%s", ssl->curves);
if(r)
goto out;
}
if(ssl->verifypeer) {
r = cf_ssl_peer_key_add_path(&buf, "CA", ssl->CAfile, &is_local);
if(r)
goto out;
r = cf_ssl_peer_key_add_path(&buf, "CApath", ssl->CApath, &is_local);
if(r)
goto out;
r = cf_ssl_peer_key_add_path(&buf, "CRL", ssl->CRLfile, &is_local);
if(r)
goto out;
r = cf_ssl_peer_key_add_path(&buf, "Issuer", ssl->issuercert, &is_local);
if(r)
goto out;
if(ssl->cert_blob) {
r = cf_ssl_peer_key_add_hash(&buf, "CertBlob", ssl->cert_blob);
if(r)
goto out;
}
if(ssl->ca_info_blob) {
r = cf_ssl_peer_key_add_hash(&buf, "CAInfoBlob", ssl->ca_info_blob);
if(r)
goto out;
}
if(ssl->issuercert_blob) {
r = cf_ssl_peer_key_add_hash(&buf, "IssuerBlob", ssl->issuercert_blob);
if(r)
goto out;
}
}
if(ssl->pinned_key && ssl->pinned_key[0]) {
r = Curl_dyn_addf(&buf, ":Pinned-%s", ssl->pinned_key);
if(r)
goto out;
}
if(ssl->clientcert && ssl->clientcert[0]) {
r = Curl_dyn_add(&buf, ":CCERT");
if(r)
goto out;
}
#ifdef USE_TLS_SRP
if(ssl->username || ssl->password) {
r = Curl_dyn_add(&buf, ":SRP-AUTH");
if(r)
goto out;
}
#endif
if(!tls_id || !tls_id[0]) {
r = CURLE_FAILED_INIT;
goto out;
}
r = Curl_dyn_addf(&buf, ":IMPL-%s", tls_id);
if(r)
goto out;
r = Curl_dyn_addf(&buf, is_local ?
CURL_SSLS_LOCAL_SUFFIX : CURL_SSLS_GLOBAL_SUFFIX);
if(r)
goto out;
*ppeer_key = Curl_dyn_take(&buf, &key_len);
/* we just added printable char, and dynbuf always 0 terminates,
* no need to track length */
out:
Curl_dyn_free(&buf);
return r;
}
static bool cf_ssl_scache_match_auth(struct Curl_ssl_scache_peer *peer,
struct ssl_primary_config *conn_config)
{
if(!conn_config) {
if(peer->clientcert)
return FALSE;
#ifdef USE_TLS_SRP
if(peer->srp_username || peer->srp_password)
return FALSE;
#endif
return TRUE;
}
else if(!Curl_safecmp(peer->clientcert, conn_config->clientcert))
return FALSE;
#ifdef USE_TLS_SRP
if(Curl_timestrcmp(peer->srp_username, conn_config->username) ||
Curl_timestrcmp(peer->srp_password, conn_config->password))
return FALSE;
#endif
return TRUE;
}
static CURLcode
cf_ssl_find_peer_by_key(struct Curl_easy *data,
struct Curl_ssl_scache *scache,
const char *ssl_peer_key,
struct ssl_primary_config *conn_config,
struct Curl_ssl_scache_peer **ppeer)
{
size_t i, peer_key_len = 0;
CURLcode result = CURLE_OK;
*ppeer = NULL;
if(!GOOD_SCACHE(scache)) {
return CURLE_BAD_FUNCTION_ARGUMENT;
}
CURL_TRC_SSLS(data, "find peer slot for %s among %zu slots",
ssl_peer_key, scache->peer_count);
/* check for entries with known peer_key */
for(i = 0; scache && i < scache->peer_count; i++) {
if(scache->peers[i].ssl_peer_key &&
strcasecompare(ssl_peer_key, scache->peers[i].ssl_peer_key) &&
cf_ssl_scache_match_auth(&scache->peers[i], conn_config)) {
/* yes, we have a cached session for this! */
*ppeer = &scache->peers[i];
goto out;
}
}
/* check for entries with HMAC set but no known peer_key */
for(i = 0; scache && i < scache->peer_count; i++) {
if(!scache->peers[i].ssl_peer_key &&
scache->peers[i].hmac_set &&
cf_ssl_scache_match_auth(&scache->peers[i], conn_config)) {
/* possible entry with unknown peer_key, check hmac */
unsigned char my_hmac[CURL_SHA256_DIGEST_LENGTH];
if(!peer_key_len) /* we are lazy */
peer_key_len = strlen(ssl_peer_key);
result = Curl_hmacit(&Curl_HMAC_SHA256,
scache->peers[i].key_salt,
sizeof(scache->peers[i].key_salt),
(const unsigned char *)ssl_peer_key,
peer_key_len,
my_hmac);
if(result)
goto out;
if(!memcmp(scache->peers[i].key_hmac, my_hmac, sizeof(my_hmac))) {
/* remember peer_key for future lookups */
CURL_TRC_SSLS(data, "peer entry %zu key recovered: %s",
i, ssl_peer_key);
scache->peers[i].ssl_peer_key = strdup(ssl_peer_key);
if(!scache->peers[i].ssl_peer_key) {
result = CURLE_OUT_OF_MEMORY;
goto out;
}
cf_ssl_cache_peer_update(&scache->peers[i]);
*ppeer = &scache->peers[i];
goto out;
}
}
}
CURL_TRC_SSLS(data, "peer not found for %s", ssl_peer_key);
out:
return result;
}
static struct Curl_ssl_scache_peer *
cf_ssl_get_free_peer(struct Curl_ssl_scache *scache)
{
struct Curl_ssl_scache_peer *peer = NULL;
size_t i;
/* find empty or oldest peer */
for(i = 0; i < scache->peer_count; ++i) {
/* free peer entry? */
if(!scache->peers[i].ssl_peer_key && !scache->peers[i].hmac_set) {
peer = &scache->peers[i];
break;
}
/* peer without sessions and obj */
if(!scache->peers[i].sobj &&
!Curl_llist_count(&scache->peers[i].sessions)) {
peer = &scache->peers[i];
break;
}
/* remember "oldest" peer */
if(!peer || (scache->peers[i].age < peer->age)) {
peer = &scache->peers[i];
}
}
DEBUGASSERT(peer);
if(peer)
cf_ssl_scache_clear_peer(peer);
return peer;
}
static CURLcode
cf_ssl_add_peer(struct Curl_easy *data,
struct Curl_ssl_scache *scache,
const char *ssl_peer_key,
struct ssl_primary_config *conn_config,
struct Curl_ssl_scache_peer **ppeer)
{
struct Curl_ssl_scache_peer *peer = NULL;
CURLcode result = CURLE_OK;
*ppeer = NULL;
if(ssl_peer_key) {
result = cf_ssl_find_peer_by_key(data, scache, ssl_peer_key, conn_config,
&peer);
if(result || !scache->peer_count)
return result;
}
if(peer) {
*ppeer = peer;
return CURLE_OK;
}
peer = cf_ssl_get_free_peer(scache);
if(peer) {
const char *ccert = conn_config ? conn_config->clientcert : NULL;
const char *username = NULL, *password = NULL;
#ifdef USE_TLS_SRP
username = conn_config ? conn_config->username : NULL;
password = conn_config ? conn_config->password : NULL;
#endif
result = cf_ssl_scache_peer_init(peer, ssl_peer_key, ccert,
username, password, NULL, NULL);
if(result)
goto out;
/* all ready */
*ppeer = peer;
result = CURLE_OK;
}
out:
if(result) {
cf_ssl_scache_clear_peer(peer);
}
return result;
}
static void cf_scache_peer_add_session(struct Curl_ssl_scache_peer *peer,
struct Curl_ssl_session *s,
curl_off_t now)
{
/* A session not from TLSv1.3 replaces all other. */
if(s->ietf_tls_id != CURL_IETF_PROTO_TLS1_3) {
Curl_llist_destroy(&peer->sessions, NULL);
Curl_llist_append(&peer->sessions, s, &s->list);
}
else {
/* Expire existing, append, trim from head to obey max_sessions */
cf_scache_peer_remove_expired(peer, now);
cf_scache_peer_remove_non13(peer);
Curl_llist_append(&peer->sessions, s, &s->list);
while(Curl_llist_count(&peer->sessions) > peer->max_sessions) {
Curl_node_remove(Curl_llist_head(&peer->sessions));
}
}
}
static CURLcode cf_scache_add_session(struct Curl_cfilter *cf,
struct Curl_easy *data,
struct Curl_ssl_scache *scache,
const char *ssl_peer_key,
struct Curl_ssl_session *s)
{
struct Curl_ssl_scache_peer *peer = NULL;
struct ssl_primary_config *conn_config = Curl_ssl_cf_get_primary_config(cf);
CURLcode result = CURLE_OUT_OF_MEMORY;
curl_off_t now = (curl_off_t)time(NULL);
curl_off_t max_lifetime;
if(!scache || !scache->peer_count) {
Curl_ssl_session_destroy(s);
return CURLE_OK;
}
if(s->valid_until <= 0)
s->valid_until = now + scache->default_lifetime_secs;
max_lifetime = (s->ietf_tls_id == CURL_IETF_PROTO_TLS1_3) ?
CURL_SCACHE_MAX_13_LIFETIME_SEC :
CURL_SCACHE_MAX_12_LIFETIME_SEC;
if(s->valid_until > (now + max_lifetime))
s->valid_until = now + max_lifetime;
if(cf_scache_session_expired(s, now)) {
CURL_TRC_SSLS(data, "add, session already expired");
Curl_ssl_session_destroy(s);
return CURLE_OK;
}
result = cf_ssl_add_peer(data, scache, ssl_peer_key, conn_config, &peer);
if(result || !peer) {
CURL_TRC_SSLS(data, "unable to add scache peer: %d", result);
Curl_ssl_session_destroy(s);
goto out;
}
cf_scache_peer_add_session(peer, s, now);
out:
if(result) {
failf(data, "[SCACHE] failed to add session for %s, error=%d",
ssl_peer_key, result);
}
else
CURL_TRC_SSLS(data, "added session for %s [proto=0x%x, "
"valid_secs=%" FMT_OFF_T ", alpn=%s, earlydata=%zu, "
"quic_tp=%s], peer has %zu sessions now",
ssl_peer_key, s->ietf_tls_id, s->valid_until - now,
s->alpn, s->earlydata_max, s->quic_tp ? "yes" : "no",
peer ? Curl_llist_count(&peer->sessions) : 0);
return result;
}
CURLcode Curl_ssl_scache_put(struct Curl_cfilter *cf,
struct Curl_easy *data,
const char *ssl_peer_key,
struct Curl_ssl_session *s)
{
struct Curl_ssl_scache *scache = cf_ssl_scache_get(data);
struct ssl_config_data *ssl_config = Curl_ssl_cf_get_config(cf, data);
CURLcode result;
DEBUGASSERT(ssl_config);
if(!scache || !ssl_config->primary.cache_session) {
Curl_ssl_session_destroy(s);
return CURLE_OK;
}
if(!GOOD_SCACHE(scache)) {
Curl_ssl_session_destroy(s);
return CURLE_BAD_FUNCTION_ARGUMENT;
}
Curl_ssl_scache_lock(data);
result = cf_scache_add_session(cf, data, scache, ssl_peer_key, s);
Curl_ssl_scache_unlock(data);
return result;
}
void Curl_ssl_scache_return(struct Curl_cfilter *cf,
struct Curl_easy *data,
const char *ssl_peer_key,
struct Curl_ssl_session *s)
{
/* See RFC 8446 C.4:
* "Clients SHOULD NOT reuse a ticket for multiple connections." */
if(s && s->ietf_tls_id < 0x304)
(void)Curl_ssl_scache_put(cf, data, ssl_peer_key, s);
else
Curl_ssl_session_destroy(s);
}
CURLcode Curl_ssl_scache_take(struct Curl_cfilter *cf,
struct Curl_easy *data,
const char *ssl_peer_key,
struct Curl_ssl_session **ps)
{
struct Curl_ssl_scache *scache = cf_ssl_scache_get(data);
struct ssl_primary_config *conn_config = Curl_ssl_cf_get_primary_config(cf);
struct Curl_ssl_scache_peer *peer = NULL;
struct Curl_llist_node *n;
struct Curl_ssl_session *s = NULL;
CURLcode result;
*ps = NULL;
if(!scache)
return CURLE_OK;
Curl_ssl_scache_lock(data);
result = cf_ssl_find_peer_by_key(data, scache, ssl_peer_key, conn_config,
&peer);
if(!result && peer) {
cf_scache_peer_remove_expired(peer, (curl_off_t)time(NULL));
n = Curl_llist_head(&peer->sessions);
if(n) {
s = Curl_node_take_elem(n);
(scache->age)++; /* increase general age */
peer->age = scache->age; /* set this as used in this age */
}
}
Curl_ssl_scache_unlock(data);
if(s) {
*ps = s;
CURL_TRC_SSLS(data, "took session for %s [proto=0x%x, "
"alpn=%s, earlydata=%zu, quic_tp=%s], %zu sessions remain",
ssl_peer_key, s->ietf_tls_id, s->alpn,
s->earlydata_max, s->quic_tp ? "yes" : "no",
Curl_llist_count(&peer->sessions));
}
else {
CURL_TRC_SSLS(data, "no cached session for %s", ssl_peer_key);
}
return result;
}
CURLcode Curl_ssl_scache_add_obj(struct Curl_cfilter *cf,
struct Curl_easy *data,
const char *ssl_peer_key,
void *sobj,
Curl_ssl_scache_obj_dtor *sobj_free)
{
struct Curl_ssl_scache *scache = cf_ssl_scache_get(data);
struct ssl_primary_config *conn_config = Curl_ssl_cf_get_primary_config(cf);
struct Curl_ssl_scache_peer *peer = NULL;
CURLcode result;
DEBUGASSERT(sobj);
DEBUGASSERT(sobj_free);
if(!scache) {
result = CURLE_BAD_FUNCTION_ARGUMENT;
goto out;
}
result = cf_ssl_add_peer(data, scache, ssl_peer_key, conn_config, &peer);
if(result || !peer) {
CURL_TRC_SSLS(data, "unable to add scache peer: %d", result);
goto out;
}
cf_ssl_scache_peer_set_obj(peer, sobj, sobj_free);
sobj = NULL; /* peer took ownership */
out:
if(sobj && sobj_free)
sobj_free(sobj);
return result;
}
void *Curl_ssl_scache_get_obj(struct Curl_cfilter *cf,
struct Curl_easy *data,
const char *ssl_peer_key)
{
struct Curl_ssl_scache *scache = cf_ssl_scache_get(data);
struct ssl_primary_config *conn_config = Curl_ssl_cf_get_primary_config(cf);
struct Curl_ssl_scache_peer *peer = NULL;
CURLcode result;
void *sobj;
if(!scache)
return NULL;
result = cf_ssl_find_peer_by_key(data, scache, ssl_peer_key, conn_config,
&peer);
if(result)
return NULL;
sobj = peer ? peer->sobj : NULL;
CURL_TRC_SSLS(data, "%s cached session for '%s'",
sobj ? "Found" : "No", ssl_peer_key);
return sobj;
}
void Curl_ssl_scache_remove_all(struct Curl_cfilter *cf,
struct Curl_easy *data,
const char *ssl_peer_key)
{
struct Curl_ssl_scache *scache = cf_ssl_scache_get(data);
struct ssl_primary_config *conn_config = Curl_ssl_cf_get_primary_config(cf);
struct Curl_ssl_scache_peer *peer = NULL;
CURLcode result;
(void)cf;
if(!scache)
return;
Curl_ssl_scache_lock(data);
result = cf_ssl_find_peer_by_key(data, scache, ssl_peer_key, conn_config,
&peer);
if(!result && peer)
cf_ssl_scache_clear_peer(peer);
Curl_ssl_scache_unlock(data);
}
#ifdef USE_SSLS_EXPORT
#define CURL_SSL_TICKET_MAX (16*1024)
static CURLcode cf_ssl_scache_peer_set_hmac(struct Curl_ssl_scache_peer *peer)
{
CURLcode result;
DEBUGASSERT(peer);
if(!peer->ssl_peer_key)
return CURLE_BAD_FUNCTION_ARGUMENT;
result = Curl_rand(NULL, peer->key_salt, sizeof(peer->key_salt));
if(result)
return result;
result = Curl_hmacit(&Curl_HMAC_SHA256,
peer->key_salt, sizeof(peer->key_salt),
(const unsigned char *)peer->ssl_peer_key,
strlen(peer->ssl_peer_key),
peer->key_hmac);
if(!result)
peer->hmac_set = TRUE;
return result;
}
static CURLcode
cf_ssl_find_peer_by_hmac(struct Curl_ssl_scache *scache,
const unsigned char *salt,
const unsigned char *hmac,
struct Curl_ssl_scache_peer **ppeer)
{
size_t i;
CURLcode result = CURLE_OK;
*ppeer = NULL;
if(!GOOD_SCACHE(scache))
return CURLE_BAD_FUNCTION_ARGUMENT;
/* look for an entry that matches salt+hmac exactly or has a known
* ssl_peer_key which salt+hmac's to the same. */
for(i = 0; scache && i < scache->peer_count; i++) {
struct Curl_ssl_scache_peer *peer = &scache->peers[i];
if(!cf_ssl_scache_match_auth(peer, NULL))
continue;
if(scache->peers[i].hmac_set &&
!memcmp(peer->key_salt, salt, sizeof(peer->key_salt)) &&
!memcmp(peer->key_hmac, hmac, sizeof(peer->key_hmac))) {
/* found exact match, return */
*ppeer = peer;
goto out;
}
else if(peer->ssl_peer_key) {
unsigned char my_hmac[CURL_SHA256_DIGEST_LENGTH];
/* compute hmac for the passed salt */
result = Curl_hmacit(&Curl_HMAC_SHA256,
salt, sizeof(peer->key_salt),
(const unsigned char *)peer->ssl_peer_key,
strlen(peer->ssl_peer_key),
my_hmac);
if(result)
goto out;
if(!memcmp(my_hmac, hmac, sizeof(my_hmac))) {
/* cryptohash match, take over salt+hmac if no set and return */
if(!peer->hmac_set) {
memcpy(peer->key_salt, salt, sizeof(peer->key_salt));
memcpy(peer->key_hmac, hmac, sizeof(peer->key_hmac));
peer->hmac_set = TRUE;
}
*ppeer = peer;
goto out;
}
}
}
out:
return result;
}
CURLcode Curl_ssl_session_import(struct Curl_easy *data,
const char *ssl_peer_key,
const unsigned char *shmac, size_t shmac_len,
const void *sdata, size_t sdata_len)
{
struct Curl_ssl_scache *scache = cf_ssl_scache_get(data);
struct Curl_ssl_scache_peer *peer = NULL;
struct Curl_ssl_session *s = NULL;
bool locked = FALSE;
CURLcode r;
if(!scache) {
r = CURLE_BAD_FUNCTION_ARGUMENT;
goto out;
}
if(!ssl_peer_key && (!shmac || !shmac_len)) {
r = CURLE_BAD_FUNCTION_ARGUMENT;
goto out;
}
r = Curl_ssl_session_unpack(data, sdata, sdata_len, &s);
if(r)
goto out;
Curl_ssl_scache_lock(data);
locked = TRUE;
if(ssl_peer_key) {
r = cf_ssl_add_peer(data, scache, ssl_peer_key, NULL, &peer);
if(r)
goto out;
}
else if(shmac_len != (sizeof(peer->key_salt) + sizeof(peer->key_hmac))) {
/* Either salt+hmac was garbled by caller or is from a curl version
* that does things differently */
r = CURLE_BAD_FUNCTION_ARGUMENT;
goto out;
}
else {
const unsigned char *salt = shmac;
const unsigned char *hmac = shmac + sizeof(peer->key_salt);
r = cf_ssl_find_peer_by_hmac(scache, salt, hmac, &peer);
if(r)
goto out;
if(!peer) {
peer = cf_ssl_get_free_peer(scache);
if(peer) {
r = cf_ssl_scache_peer_init(peer, ssl_peer_key, NULL,
NULL, NULL, salt, hmac);
if(r)
goto out;
}
}
}
if(peer) {
cf_scache_peer_add_session(peer, s, time(NULL));
s = NULL; /* peer is now owner */
CURL_TRC_SSLS(data, "successfully imported ticket for peer %s, now "
"with %zu tickets",
peer->ssl_peer_key ? peer->ssl_peer_key : "without key",
Curl_llist_count(&peer->sessions));
}
out:
if(locked)
Curl_ssl_scache_unlock(data);
Curl_ssl_session_destroy(s);
return r;
}
CURLcode Curl_ssl_session_export(struct Curl_easy *data,
curl_ssls_export_cb *export_fn,
void *userptr)
{
struct Curl_ssl_scache *scache = cf_ssl_scache_get(data);
struct Curl_ssl_scache_peer *peer;
struct dynbuf sbuf, hbuf;
struct Curl_llist_node *n;
size_t i, npeers = 0, ntickets = 0;
curl_off_t now = time(NULL);
CURLcode r = CURLE_OK;
if(!export_fn)
return CURLE_BAD_FUNCTION_ARGUMENT;
if(!scache)
return CURLE_OK;
Curl_ssl_scache_lock(data);
Curl_dyn_init(&hbuf, (CURL_SHA256_DIGEST_LENGTH * 2) + 1);
Curl_dyn_init(&sbuf, CURL_SSL_TICKET_MAX);
for(i = 0; scache && i < scache->peer_count; i++) {
peer = &scache->peers[i];
if(!peer->ssl_peer_key && !peer->hmac_set)
continue; /* skip free entry */
if(!peer->exportable)
continue;
Curl_dyn_reset(&hbuf);
cf_scache_peer_remove_expired(peer, now);
n = Curl_llist_head(&peer->sessions);
if(n)
++npeers;
while(n) {
struct Curl_ssl_session *s = Curl_node_elem(n);
if(!peer->hmac_set) {
r = cf_ssl_scache_peer_set_hmac(peer);
if(r)
goto out;
}
if(!Curl_dyn_len(&hbuf)) {
r = Curl_dyn_addn(&hbuf, peer->key_salt, sizeof(peer->key_salt));
if(r)
goto out;
r = Curl_dyn_addn(&hbuf, peer->key_hmac, sizeof(peer->key_hmac));
if(r)
goto out;
}
Curl_dyn_reset(&sbuf);
r = Curl_ssl_session_pack(data, s, &sbuf);
if(r)
goto out;
r = export_fn(data, userptr, peer->ssl_peer_key,
Curl_dyn_uptr(&hbuf), Curl_dyn_len(&hbuf),
Curl_dyn_uptr(&sbuf), Curl_dyn_len(&sbuf),
s->valid_until, s->ietf_tls_id,
s->alpn, s->earlydata_max);
if(r)
goto out;
++ntickets;
n = Curl_node_next(n);
}
}
r = CURLE_OK;
CURL_TRC_SSLS(data, "exported %zu session tickets for %zu peers",
ntickets, npeers);
out:
Curl_ssl_scache_unlock(data);
Curl_dyn_free(&hbuf);
Curl_dyn_free(&sbuf);
return r;
}
#endif /* USE_SSLS_EXPORT */
#endif /* USE_SSL */