new file mode 100644
@@ -0,0 +1,71 @@
+From a79f8dae1aa632155be430143b4685f0b91d1694 Mon Sep 17 00:00:00 2001
+From: Daniel Stenberg <daniel@haxx.se>
+Date: Wed, 6 May 2026 23:59:22 +0200
+Subject: [PATCH] mbedtls: null terminate the private key blob
+
+Unfortunately, mbedtls_pk_parse_key() requires the data to be
+null-terminated if the data is PEM encoded (even when provided the exact
+length), so this function needs to make a copy that has one.
+
+Reported-by: Elise Vance
+Closes #21515
+
+CVE: CVE-2026-8932
+Upstream-Status: Backport [https://github.com/curl/curl/commit/67bf021e97bbebadade7e40217a4967042ac6a07]
+
+Backport Changes:
+- Added as a prerequisite for upstream commit [1] because Wrynose curl
+ 8.19.0 did not already contain this mbedTLS private-key blob fix.
+
+[1] https://github.com/curl/curl/commit/7541ae569d82fb308a5e2d94916027da4fa3ba3e
+
+(cherry picked from commit 67bf021e97bbebadade7e40217a4967042ac6a07)
+Signed-off-by: Deepak Rathore <deeratho@cisco.com>
+---
+ lib/vtls/mbedtls.c | 15 +++++++++++----
+ 1 file changed, 11 insertions(+), 4 deletions(-)
+
+diff --git a/lib/vtls/mbedtls.c b/lib/vtls/mbedtls.c
+index 2bac406c35..54f0ac9fd4 100644
+--- a/lib/vtls/mbedtls.c
++++ b/lib/vtls/mbedtls.c
+@@ -697,11 +697,17 @@ static CURLcode mbed_load_privkey(struct Curl_cfilter *cf,
+ }
+ else {
+ const struct curl_blob *ssl_key_blob = ssl_config->key_blob;
+- const unsigned char *key_data =
+- (const unsigned char *)ssl_key_blob->data;
+ const char *passwd = ssl_config->key_passwd;
++ /* Unfortunately, mbedtls_pk_parse_key() requires the data to be
++ null-terminated if the data is PEM encoded (even when provided the
++ exact length). */
++ unsigned char *newblob = curlx_memdup0(ssl_key_blob->data,
++ ssl_key_blob->len);
++ if(!newblob)
++ return CURLE_OUT_OF_MEMORY;
++
+ #if MBEDTLS_VERSION_NUMBER >= 0x04000000
+- ret = mbedtls_pk_parse_key(&backend->pk, key_data, ssl_key_blob->len,
++ ret = mbedtls_pk_parse_key(&backend->pk, newblob, ssl_key_blob->len,
+ (const unsigned char *)passwd,
+ passwd ? strlen(passwd) : 0);
+ if(ret == 0 &&
+@@ -713,7 +719,7 @@ static CURLcode mbed_load_privkey(struct Curl_cfilter *cf,
+ PSA_KEY_USAGE_SIGN_HASH)))
+ ret = MBEDTLS_ERR_PK_TYPE_MISMATCH;
+ #else
+- ret = mbedtls_pk_parse_key(&backend->pk, key_data, ssl_key_blob->len,
++ ret = mbedtls_pk_parse_key(&backend->pk, newblob, ssl_key_blob->len,
+ (const unsigned char *)passwd,
+ passwd ? strlen(passwd) : 0,
+ mbedtls_ctr_drbg_random,
+@@ -722,6 +728,7 @@ static CURLcode mbed_load_privkey(struct Curl_cfilter *cf,
+ mbedtls_pk_can_do(&backend->pk, MBEDTLS_PK_ECKEY)))
+ ret = MBEDTLS_ERR_PK_TYPE_MISMATCH;
+ #endif
++ curlx_free(newblob);
+
+ if(ret) {
+ mbedtls_strerror(ret, errorbuf, sizeof(errorbuf));
+--
+2.35.6
new file mode 100644
@@ -0,0 +1,1148 @@
+From c1cf1ddf723cdfcbd071394a6b4e73ea995e100a Mon Sep 17 00:00:00 2001
+From: Joshua Rogers <MegaManSec@users.noreply.github.com>
+Date: Tue, 14 Jul 2026 04:38:15 -0700
+Subject: [PATCH] tls: fix incomplete mTLS config in conn reuse and session
+ cache
+
+cert_type, key, key_type, key_passwd and key_blob lived in
+ssl_config_data but not in ssl_primary_config, so they were invisible to
+match_ssl_primary_config() and to the TLS session cache peer key.
+
+Two easy handles sharing a connection pool could reuse each other's
+authenticated connections when they differed only on SSLKEY, SSLKEYTYPE,
+KEYPASSWD, SSLCERTTYPE or SSLKEYBLOB. The second handle would silently
+inherit the first handle's authenticated identity.
+
+Promote all five fields into ssl_primary_config so the conn-reuse
+predicate and session cache key cover the complete client credential
+set. Also replace the fixed ":CCERT" session cache marker with the
+actual clientcert path so sessions are not shared across different
+client certificates.
+
+Verified by test 3303 and 3304
+
+Reported-By: Joshua Rogers (AISLE Research)
+Closes #21667
+
+CVE: CVE-2026-8932
+Upstream-Status: Backport [https://github.com/curl/curl/commit/7541ae569d82fb308a5e2d94916027da4fa3ba3e]
+
+Backport Changes:
+- Split prerequisite upstream commit [1] before this patch. This patch
+ now applies on top of that dependency and does not fold in the
+ mbedTLS private-key blob null-termination change.
+- Kept Wrynose Curl_safefree() helper usage and local result-variable
+ style while preserving the upstream mTLS credential matching and
+ session-cache key behavior.
+- Wrynose curl 8.19.0 does not have upstream Curl_peer. The
+ Curl_ssl_peer_key_build() helper keeps Wrynose connectdata and
+ conn_to_host/conn_to_port keying while adding the mTLS key fields.
+- Kept Wrynose test registry ordering and added only the new upstream
+ test3303/test3304 and unit3303/unit3304 regression test entries.
+
+[1] https://github.com/curl/curl/commit/67bf021e97bbebadade7e40217a4967042ac6a07
+
+(cherry picked from commit 7541ae569d82fb308a5e2d94916027da4fa3ba3e)
+Signed-off-by: Deepak Rathore <deeratho@cisco.com>
+---
+ lib/ldap.c | 4 +-
+ lib/urldata.h | 10 +--
+ lib/vssh/libssh.c | 6 +-
+ lib/vssh/libssh2.c | 2 +-
+ lib/vtls/gtls.c | 14 ++--
+ lib/vtls/mbedtls.c | 24 +++---
+ lib/vtls/openssl.c | 7 +-
+ lib/vtls/rustls.c | 13 ++--
+ lib/vtls/schannel.c | 10 +--
+ lib/vtls/vtls.c | 37 ++++++---
+ lib/vtls/vtls_scache.c | 105 +++++++++++++++++++++-----
+ lib/vtls/vtls_scache.h | 16 ++++
+ lib/vtls/wolfssl.c | 14 ++--
+ tests/data/Makefile.am | 3 +
+ tests/data/test3303 | 20 +++++
+ tests/data/test3304 | 20 +++++
+ tests/unit/Makefile.inc | 3 +-
+ tests/unit/unit3303.c | 127 +++++++++++++++++++++++++++++++
+ tests/unit/unit3304.c | 163 ++++++++++++++++++++++++++++++++++++++++
+ 19 files changed, 517 insertions(+), 81 deletions(-)
+ create mode 100644 tests/data/test3303
+ create mode 100644 tests/data/test3304
+ create mode 100644 tests/unit/unit3303.c
+ create mode 100644 tests/unit/unit3304.c
+
+diff --git a/lib/ldap.c b/lib/ldap.c
+index e223078b03..5bf76572c8 100644
+--- a/lib/ldap.c
++++ b/lib/ldap.c
+@@ -329,8 +329,8 @@ static CURLcode ldap_do(struct Curl_easy *data, bool *done)
+ #ifdef LDAP_OPT_X_TLS
+ if(conn->ssl_config.verifypeer) {
+ /* OpenLDAP SDK supports BASE64 files. */
+- if(data->set.ssl.cert_type &&
+- !curl_strequal(data->set.ssl.cert_type, "PEM")) {
++ if(data->set.ssl.primary.cert_type &&
++ !curl_strequal(data->set.ssl.primary.cert_type, "PEM")) {
+ failf(data, "LDAP local: ERROR OpenLDAP only supports PEM cert-type");
+ result = CURLE_SSL_CERTPROBLEM;
+ goto quit;
+diff --git a/lib/urldata.h b/lib/urldata.h
+index 65005c4b34..dce181d99d 100644
+--- a/lib/urldata.h
++++ b/lib/urldata.h
+@@ -230,9 +230,14 @@ struct ssl_primary_config {
+ char *signature_algorithms; /* list of signature algorithms to use */
+ char *pinned_key;
+ char *CRLfile; /* CRL to check certificate revocation */
++ char *cert_type; /* format for certificate (default: PEM) */
++ char *key; /* private key filename */
++ char *key_type; /* format for private key (default: PEM) */
++ char *key_passwd; /* plain text private key password */
+ struct curl_blob *cert_blob;
+ struct curl_blob *ca_info_blob;
+ struct curl_blob *issuercert_blob;
++ struct curl_blob *key_blob;
+ #ifdef USE_TLS_SRP
+ char *username; /* TLS username (for, e.g., SRP) */
+ char *password; /* TLS password (for, e.g., SRP) */
+@@ -252,11 +257,6 @@ struct ssl_config_data {
+ long certverifyresult; /* result from the certificate verification */
+ curl_ssl_ctx_callback fsslctx; /* function to initialize ssl ctx */
+ void *fsslctxp; /* parameter for call back */
+- char *cert_type; /* format for certificate (default: PEM) */
+- char *key; /* private key filename */
+- struct curl_blob *key_blob;
+- char *key_type; /* format for private key (default: PEM) */
+- char *key_passwd; /* plain text private key password */
+ BIT(certinfo); /* gather lots of certificate info */
+ BIT(earlydata); /* use TLS 1.3 early data */
+ BIT(enable_beast); /* allow this flaw for interoperability's sake */
+diff --git a/lib/vssh/libssh.c b/lib/vssh/libssh.c
+index 3a2a52e1f8..db767f7d00 100644
+--- a/lib/vssh/libssh.c
++++ b/lib/vssh/libssh.c
+@@ -791,7 +791,7 @@ static int myssh_in_AUTH_PKEY_INIT(struct Curl_easy *data,
+ /* Two choices, (1) private key was given on CMD,
+ * (2) use the "default" keys. */
+ if(data->set.str[STRING_SSH_PRIVATE_KEY]) {
+- if(sshc->pubkey && !data->set.ssl.key_passwd) {
++ if(sshc->pubkey && !data->set.ssl.primary.key_passwd) {
+ rc = ssh_userauth_try_publickey(sshc->ssh_session, NULL, sshc->pubkey);
+ if(rc == SSH_AUTH_AGAIN)
+ return SSH_AGAIN;
+@@ -804,7 +804,7 @@ static int myssh_in_AUTH_PKEY_INIT(struct Curl_easy *data,
+
+ rc = ssh_pki_import_privkey_file(data->
+ set.str[STRING_SSH_PRIVATE_KEY],
+- data->set.ssl.key_passwd, NULL,
++ data->set.ssl.primary.key_passwd, NULL,
+ NULL, &sshc->privkey);
+ if(rc != SSH_OK) {
+ failf(data, "Could not load private key file %s",
+@@ -817,7 +817,7 @@ static int myssh_in_AUTH_PKEY_INIT(struct Curl_easy *data,
+ }
+ else {
+ rc = ssh_userauth_publickey_auto(sshc->ssh_session, NULL,
+- data->set.ssl.key_passwd);
++ data->set.ssl.primary.key_passwd);
+ if(rc == SSH_AUTH_AGAIN)
+ return SSH_AGAIN;
+
+diff --git a/lib/vssh/libssh2.c b/lib/vssh/libssh2.c
+index 63f5735b83..3c25fe7448 100644
+--- a/lib/vssh/libssh2.c
++++ b/lib/vssh/libssh2.c
+@@ -1144,7 +1144,7 @@ static CURLcode ssh_state_pkey_init(struct Curl_easy *data,
+ return CURLE_OUT_OF_MEMORY;
+ }
+
+- sshc->passphrase = data->set.ssl.key_passwd;
++ sshc->passphrase = data->set.ssl.primary.key_passwd;
+ if(!sshc->passphrase)
+ sshc->passphrase = "";
+
+diff --git a/lib/vtls/gtls.c b/lib/vtls/gtls.c
+index a0a465c360..bfeb948542 100644
+--- a/lib/vtls/gtls.c
++++ b/lib/vtls/gtls.c
+@@ -988,10 +988,11 @@ static CURLcode gtls_client_init(struct Curl_cfilter *cf,
+ if(result)
+ return result;
+ }
+- if(ssl_config->cert_type && curl_strequal(ssl_config->cert_type, "P12")) {
++ if(ssl_config->primary.cert_type &&
++ curl_strequal(ssl_config->primary.cert_type, "P12")) {
+ rc = gnutls_certificate_set_x509_simple_pkcs12_file(
+ gtls->shared_creds->creds, config->clientcert, GNUTLS_X509_FMT_DER,
+- ssl_config->key_passwd ? ssl_config->key_passwd : "");
++ ssl_config->primary.key_passwd ? ssl_config->primary.key_passwd : "");
+ if(rc != GNUTLS_E_SUCCESS) {
+ failf(data,
+ "error reading X.509 potentially-encrypted key or certificate "
+@@ -1009,14 +1010,15 @@ static CURLcode gtls_client_init(struct Curl_cfilter *cf,
+ rc = gnutls_certificate_set_x509_key_file2(
+ gtls->shared_creds->creds,
+ config->clientcert,
+- ssl_config->key ? ssl_config->key : config->clientcert,
+- gnutls_do_file_type(ssl_config->cert_type),
+- ssl_config->key_passwd,
++ ssl_config->primary.key ? ssl_config->primary.key :
++ config->clientcert,
++ gnutls_do_file_type(ssl_config->primary.cert_type),
++ ssl_config->primary.key_passwd,
+ supported_key_encryption_algorithms);
+ if(rc != GNUTLS_E_SUCCESS) {
+ failf(data,
+ "error reading X.509 %skey file: %s",
+- ssl_config->key_passwd ? "potentially-encrypted " : "",
++ ssl_config->primary.key_passwd ? "potentially-encrypted " : "",
+ gnutls_strerror(rc));
+ return CURLE_SSL_CONNECT_ERROR;
+ }
+diff --git a/lib/vtls/mbedtls.c b/lib/vtls/mbedtls.c
+index 54f0ac9fd4..d2199df884 100644
+--- a/lib/vtls/mbedtls.c
++++ b/lib/vtls/mbedtls.c
+@@ -484,7 +484,7 @@ static CURLcode mbed_load_cacert(struct Curl_cfilter *cf,
+ const char * const ssl_capath = conn_config->CApath;
+ #ifdef MBEDTLS_PEM_PARSE_C
+ struct ssl_config_data *ssl_config = Curl_ssl_cf_get_config(cf, data);
+- const char * const ssl_cert_type = ssl_config->cert_type;
++ const char * const ssl_cert_type = ssl_config->primary.cert_type;
+ #endif
+ int ret = -1;
+ char errorbuf[128];
+@@ -579,7 +579,7 @@ static CURLcode mbed_load_clicert(struct Curl_cfilter *cf,
+ char * const ssl_cert = ssl_config->primary.clientcert;
+ const struct curl_blob *ssl_cert_blob = ssl_config->primary.cert_blob;
+ #ifdef MBEDTLS_PEM_PARSE_C
+- const char * const ssl_cert_type = ssl_config->cert_type;
++ const char * const ssl_cert_type = ssl_config->primary.cert_type;
+ #endif
+ int ret = -1;
+ char errorbuf[128];
+@@ -660,12 +660,12 @@ static CURLcode mbed_load_privkey(struct Curl_cfilter *cf,
+
+ mbedtls_pk_init(&backend->pk);
+
+- if(ssl_config->key || ssl_config->key_blob) {
+- if(ssl_config->key) {
++ if(ssl_config->primary.key || ssl_config->primary.key_blob) {
++ if(ssl_config->primary.key) {
+ #ifdef MBEDTLS_FS_IO
+ #if MBEDTLS_VERSION_NUMBER >= 0x04000000
+- ret = mbedtls_pk_parse_keyfile(&backend->pk, ssl_config->key,
+- ssl_config->key_passwd);
++ ret = mbedtls_pk_parse_keyfile(&backend->pk, ssl_config->primary.key,
++ ssl_config->primary.key_passwd);
+ if(ret == 0 &&
+ !(mbedtls_pk_can_do_psa(&backend->pk,
+ PSA_ALG_RSA_PKCS1V15_SIGN(PSA_ALG_ANY_HASH),
+@@ -675,8 +675,8 @@ static CURLcode mbed_load_privkey(struct Curl_cfilter *cf,
+ PSA_KEY_USAGE_SIGN_HASH)))
+ ret = MBEDTLS_ERR_PK_TYPE_MISMATCH;
+ #else
+- ret = mbedtls_pk_parse_keyfile(&backend->pk, ssl_config->key,
+- ssl_config->key_passwd,
++ ret = mbedtls_pk_parse_keyfile(&backend->pk, ssl_config->primary.key,
++ ssl_config->primary.key_passwd,
+ mbedtls_ctr_drbg_random,
+ &rng.drbg);
+ if(ret == 0 && !(mbedtls_pk_can_do(&backend->pk, MBEDTLS_PK_RSA) ||
+@@ -687,7 +687,7 @@ static CURLcode mbed_load_privkey(struct Curl_cfilter *cf,
+ if(ret) {
+ mbedtls_strerror(ret, errorbuf, sizeof(errorbuf));
+ failf(data, "mbedTLS: error reading private key %s: (-0x%04X) %s",
+- ssl_config->key, -ret, errorbuf);
++ ssl_config->primary.key, -ret, errorbuf);
+ return CURLE_SSL_CERTPROBLEM;
+ }
+ #else
+@@ -696,8 +696,8 @@ static CURLcode mbed_load_privkey(struct Curl_cfilter *cf,
+ #endif
+ }
+ else {
+- const struct curl_blob *ssl_key_blob = ssl_config->key_blob;
+- const char *passwd = ssl_config->key_passwd;
++ const struct curl_blob *ssl_key_blob = ssl_config->primary.key_blob;
++ const char *passwd = ssl_config->primary.key_passwd;
+ /* Unfortunately, mbedtls_pk_parse_key() requires the data to be
+ null-terminated if the data is PEM encoded (even when provided the
+ exact length). */
+@@ -931,7 +931,7 @@ static CURLcode mbed_configure_ssl(struct Curl_cfilter *cf,
+ #endif
+ );
+
+- if(ssl_config->key || ssl_config->key_blob) {
++ if(ssl_config->primary.key || ssl_config->primary.key_blob) {
+ mbedtls_ssl_conf_own_cert(&backend->config, &backend->clicert,
+ &backend->pk);
+ }
+diff --git a/lib/vtls/openssl.c b/lib/vtls/openssl.c
+index 50bf1e0476..9cbab14b41 100644
+--- a/lib/vtls/openssl.c
++++ b/lib/vtls/openssl.c
+@@ -3724,7 +3724,7 @@ CURLcode Curl_ossl_ctx_init(struct ossl_ctx *octx,
+ struct ssl_config_data *ssl_config = Curl_ssl_cf_get_config(cf, data);
+ char * const ssl_cert = ssl_config->primary.clientcert;
+ const struct curl_blob *ssl_cert_blob = ssl_config->primary.cert_blob;
+- const char * const ssl_cert_type = ssl_config->cert_type;
++ const char * const ssl_cert_type = ssl_config->primary.cert_type;
+ unsigned int ssl_version_min;
+ char error_buffer[256];
+
+@@ -3888,8 +3888,9 @@ CURLcode Curl_ossl_ctx_init(struct ossl_ctx *octx,
+ if(ssl_cert || ssl_cert_blob || ssl_cert_type) {
+ result = client_cert(data, octx->ssl_ctx,
+ ssl_cert, ssl_cert_blob, ssl_cert_type,
+- ssl_config->key, ssl_config->key_blob,
+- ssl_config->key_type, ssl_config->key_passwd);
++ ssl_config->primary.key, ssl_config->primary.key_blob,
++ ssl_config->primary.key_type,
++ ssl_config->primary.key_passwd);
+ if(result)
+ /* failf() is already done in client_cert() */
+ return result;
+diff --git a/lib/vtls/rustls.c b/lib/vtls/rustls.c
+index 061b444bce..564b10aa47 100644
+--- a/lib/vtls/rustls.c
++++ b/lib/vtls/rustls.c
+@@ -841,14 +841,14 @@ init_config_builder_client_auth(struct Curl_easy *data,
+ const struct rustls_certified_key *certified_key = NULL;
+ CURLcode result = CURLE_OK;
+
+- if(conn_config->clientcert && !ssl_config->key) {
++ if(conn_config->clientcert && !ssl_config->primary.key) {
+ failf(data, "rustls: must provide key with certificate '%s'",
+ conn_config->clientcert);
+ return CURLE_SSL_CERTPROBLEM;
+ }
+- else if(!conn_config->clientcert && ssl_config->key) {
++ else if(!conn_config->clientcert && ssl_config->primary.key) {
+ failf(data, "rustls: must provide certificate with key '%s'",
+- ssl_config->key);
++ ssl_config->primary.key);
+ return CURLE_SSL_CERTPROBLEM;
+ }
+
+@@ -862,8 +862,9 @@ init_config_builder_client_auth(struct Curl_easy *data,
+ goto cleanup;
+ }
+
+- if(!read_file_into(ssl_config->key, &key_contents)) {
+- failf(data, "rustls: failed to read key file: '%s'", ssl_config->key);
++ if(!read_file_into(ssl_config->primary.key, &key_contents)) {
++ failf(data, "rustls: failed to read key file: '%s'",
++ ssl_config->primary.key);
+ result = CURLE_SSL_CERTPROBLEM;
+ goto cleanup;
+ }
+@@ -1053,7 +1054,7 @@ static CURLcode cr_init_backend(struct Curl_cfilter *cf,
+ }
+ }
+
+- if(conn_config->clientcert || ssl_config->key) {
++ if(conn_config->clientcert || ssl_config->primary.key) {
+ result = init_config_builder_client_auth(data,
+ conn_config,
+ ssl_config,
+diff --git a/lib/vtls/schannel.c b/lib/vtls/schannel.c
+index 650a0e38ca..a4f57c7c0a 100644
+--- a/lib/vtls/schannel.c
++++ b/lib/vtls/schannel.c
+@@ -414,8 +414,8 @@ static CURLcode get_client_cert(struct Curl_easy *data,
+ }
+ }
+
+- if((fInCert || blob) && data->set.ssl.cert_type &&
+- !curl_strequal(data->set.ssl.cert_type, "P12")) {
++ if((fInCert || blob) && data->set.ssl.primary.cert_type &&
++ !curl_strequal(data->set.ssl.primary.cert_type, "P12")) {
+ failf(data, "schannel: certificate format compatibility error "
+ "for %s",
+ blob ? "(memory blob)" : data->set.ssl.primary.clientcert);
+@@ -465,15 +465,15 @@ static CURLcode get_client_cert(struct Curl_easy *data,
+ datablob.pbData = (BYTE *)certdata;
+ datablob.cbData = (DWORD)certsize;
+
+- if(data->set.ssl.key_passwd)
+- pwd_len = strlen(data->set.ssl.key_passwd);
++ if(data->set.ssl.primary.key_passwd)
++ pwd_len = strlen(data->set.ssl.primary.key_passwd);
+ pszPassword = (WCHAR *)curlx_malloc(sizeof(WCHAR) * (pwd_len + 1));
+ if(pszPassword) {
+ int str_w_len = 0;
+ if(pwd_len > 0)
+ str_w_len = MultiByteToWideChar(CP_UTF8,
+ MB_ERR_INVALID_CHARS,
+- data->set.ssl.key_passwd,
++ data->set.ssl.primary.key_passwd,
+ (int)pwd_len,
+ pszPassword, (int)(pwd_len + 1));
+
+diff --git a/lib/vtls/vtls.c b/lib/vtls/vtls.c
+index f7201d18d6..6dcc67dccd 100644
+--- a/lib/vtls/vtls.c
++++ b/lib/vtls/vtls.c
+@@ -204,6 +204,7 @@ static bool match_ssl_primary_config(struct Curl_easy *data,
+ blobcmp(c1->cert_blob, c2->cert_blob) &&
+ blobcmp(c1->ca_info_blob, c2->ca_info_blob) &&
+ blobcmp(c1->issuercert_blob, c2->issuercert_blob) &&
++ blobcmp(c1->key_blob, c2->key_blob) &&
+ Curl_safecmp(c1->CApath, c2->CApath) &&
+ Curl_safecmp(c1->CAfile, c2->CAfile) &&
+ Curl_safecmp(c1->issuercert, c2->issuercert) &&
+@@ -217,7 +218,11 @@ static bool match_ssl_primary_config(struct Curl_easy *data,
+ curl_strequal(c1->curves, c2->curves) &&
+ curl_strequal(c1->signature_algorithms, c2->signature_algorithms) &&
+ curl_strequal(c1->CRLfile, c2->CRLfile) &&
+- curl_strequal(c1->pinned_key, c2->pinned_key))
++ curl_strequal(c1->pinned_key, c2->pinned_key) &&
++ curl_strequal(c1->cert_type, c2->cert_type) &&
++ Curl_safecmp(c1->key, c2->key) &&
++ curl_strequal(c1->key_type, c2->key_type) &&
++ !Curl_timestrcmp(c1->key_passwd, c2->key_passwd))
+ return TRUE;
+
+ return FALSE;
+@@ -252,6 +257,7 @@ static bool clone_ssl_primary_config(struct ssl_primary_config *source,
+ CLONE_BLOB(cert_blob);
+ CLONE_BLOB(ca_info_blob);
+ CLONE_BLOB(issuercert_blob);
++ CLONE_BLOB(key_blob);
+ CLONE_STRING(CApath);
+ CLONE_STRING(CAfile);
+ CLONE_STRING(issuercert);
+@@ -262,6 +268,10 @@ static bool clone_ssl_primary_config(struct ssl_primary_config *source,
+ CLONE_STRING(curves);
+ CLONE_STRING(signature_algorithms);
+ CLONE_STRING(CRLfile);
++ CLONE_STRING(cert_type);
++ CLONE_STRING(key);
++ CLONE_STRING(key_type);
++ CLONE_STRING(key_passwd);
+ #ifdef USE_TLS_SRP
+ CLONE_STRING(username);
+ CLONE_STRING(password);
+@@ -282,9 +292,14 @@ static void free_primary_ssl_config(struct ssl_primary_config *sslc)
+ Curl_safefree(sslc->cert_blob);
+ Curl_safefree(sslc->ca_info_blob);
+ Curl_safefree(sslc->issuercert_blob);
++ Curl_safefree(sslc->key_blob);
+ Curl_safefree(sslc->curves);
+ Curl_safefree(sslc->signature_algorithms);
+ Curl_safefree(sslc->CRLfile);
++ Curl_safefree(sslc->cert_type);
++ Curl_safefree(sslc->key);
++ Curl_safefree(sslc->key_type);
++ Curl_safefree(sslc->key_passwd);
+ #ifdef USE_TLS_SRP
+ Curl_safefree(sslc->username);
+ Curl_safefree(sslc->password);
+@@ -336,12 +351,12 @@ CURLcode Curl_ssl_easy_config_complete(struct Curl_easy *data)
+ sslc->primary.username = data->set.str[STRING_TLSAUTH_USERNAME];
+ sslc->primary.password = data->set.str[STRING_TLSAUTH_PASSWORD];
+ #endif
+- sslc->cert_type = data->set.str[STRING_CERT_TYPE];
+- sslc->key = data->set.str[STRING_KEY];
+- sslc->key_type = data->set.str[STRING_KEY_TYPE];
+- sslc->key_passwd = data->set.str[STRING_KEY_PASSWD];
++ sslc->primary.cert_type = data->set.str[STRING_CERT_TYPE];
++ sslc->primary.key = data->set.str[STRING_KEY];
++ sslc->primary.key_type = data->set.str[STRING_KEY_TYPE];
++ sslc->primary.key_passwd = data->set.str[STRING_KEY_PASSWD];
+ sslc->primary.clientcert = data->set.str[STRING_CERT];
+- sslc->key_blob = data->set.blobs[BLOB_KEY];
++ sslc->primary.key_blob = data->set.blobs[BLOB_KEY];
+
+ #ifndef CURL_DISABLE_PROXY
+ sslc = &data->set.proxy_ssl;
+@@ -377,12 +392,12 @@ CURLcode Curl_ssl_easy_config_complete(struct Curl_easy *data)
+ sslc->primary.issuercert = data->set.str[STRING_SSL_ISSUERCERT_PROXY];
+ sslc->primary.issuercert_blob = data->set.blobs[BLOB_SSL_ISSUERCERT_PROXY];
+ sslc->primary.CRLfile = data->set.str[STRING_SSL_CRLFILE_PROXY];
+- sslc->cert_type = data->set.str[STRING_CERT_TYPE_PROXY];
+- sslc->key = data->set.str[STRING_KEY_PROXY];
+- sslc->key_type = data->set.str[STRING_KEY_TYPE_PROXY];
+- sslc->key_passwd = data->set.str[STRING_KEY_PASSWD_PROXY];
++ sslc->primary.cert_type = data->set.str[STRING_CERT_TYPE_PROXY];
++ sslc->primary.key = data->set.str[STRING_KEY_PROXY];
++ sslc->primary.key_type = data->set.str[STRING_KEY_TYPE_PROXY];
++ sslc->primary.key_passwd = data->set.str[STRING_KEY_PASSWD_PROXY];
+ sslc->primary.clientcert = data->set.str[STRING_CERT_PROXY];
+- sslc->key_blob = data->set.blobs[BLOB_KEY_PROXY];
++ sslc->primary.key_blob = data->set.blobs[BLOB_KEY_PROXY];
+ #ifdef USE_TLS_SRP
+ sslc->primary.username = data->set.str[STRING_TLSAUTH_USERNAME_PROXY];
+ sslc->primary.password = data->set.str[STRING_TLSAUTH_PASSWORD_PROXY];
+diff --git a/lib/vtls/vtls_scache.c b/lib/vtls/vtls_scache.c
+index 673abcfe0d..75e5b89553 100644
+--- a/lib/vtls/vtls_scache.c
++++ b/lib/vtls/vtls_scache.c
+@@ -50,6 +50,7 @@
+ struct Curl_ssl_scache_peer {
+ char *ssl_peer_key; /* id for peer + relevant TLS configuration */
+ char *clientcert;
++ char *key_passwd;
+ char *srp_username;
+ char *srp_password;
+ struct Curl_llist sessions;
+@@ -123,6 +124,48 @@ out:
+ return r;
+ }
+
++static CURLcode cf_ssl_peer_key_add_mtls(struct dynbuf *buf,
++ struct ssl_primary_config *ssl,
++ bool *is_local)
++{
++ CURLcode result = CURLE_OK;
++ if(ssl->clientcert && ssl->clientcert[0]) {
++ result = cf_ssl_peer_key_add_path(buf, "CCERT", ssl->clientcert, is_local);
++ if(result)
++ goto out;
++ }
++ if(ssl->key && ssl->key[0]) {
++ result = cf_ssl_peer_key_add_path(buf, "KEY", ssl->key, is_local);
++ if(result)
++ goto out;
++ }
++ if(ssl->key_blob) {
++ result = cf_ssl_peer_key_add_hash(buf, "KEYBlob", ssl->key_blob);
++ if(result)
++ goto out;
++ }
++ if(ssl->cert_type && ssl->cert_type[0]) {
++ size_t i;
++ result = curlx_dyn_add(buf, ":CT-");
++ for(i = 0; !result && ssl->cert_type[i]; i++) {
++ char c = Curl_raw_toupper(ssl->cert_type[i]);
++ result = curlx_dyn_addn(buf, &c, 1);
++ }
++ if(result)
++ goto out;
++ }
++ if(ssl->key_type && ssl->key_type[0]) {
++ size_t i;
++ result = curlx_dyn_add(buf, ":KT-");
++ for(i = 0; !result && ssl->key_type[i]; i++) {
++ char c = Curl_raw_toupper(ssl->key_type[i]);
++ result = curlx_dyn_addn(buf, &c, 1);
++ }
++ }
++out:
++ return result;
++}
++
+ #define CURL_SSLS_LOCAL_SUFFIX ":L"
+ #define CURL_SSLS_GLOBAL_SUFFIX ":G"
+
+@@ -134,12 +177,12 @@ static bool cf_ssl_peer_key_is_global(const char *peer_key)
+ (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)
++CURLcode Curl_ssl_peer_key_build(struct ssl_primary_config *ssl,
++ const struct ssl_peer *peer,
++ const struct connectdata *conn,
++ 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;
+@@ -187,13 +230,13 @@ CURLcode Curl_ssl_peer_key_make(struct Curl_cfilter *cf,
+ goto out;
+ }
+ if(!ssl->verifypeer || !ssl->verifyhost) {
+- if(cf->conn->bits.conn_to_host) {
+- r = curlx_dyn_addf(&buf, ":CHOST-%s", cf->conn->conn_to_host.name);
++ if(conn && conn->bits.conn_to_host) {
++ r = curlx_dyn_addf(&buf, ":CHOST-%s", conn->conn_to_host.name);
+ if(r)
+ goto out;
+ }
+- if(cf->conn->bits.conn_to_port) {
+- r = curlx_dyn_addf(&buf, ":CPORT-%d", cf->conn->conn_to_port);
++ if(conn && conn->bits.conn_to_port) {
++ r = curlx_dyn_addf(&buf, ":CPORT-%d", conn->conn_to_port);
+ if(r)
+ goto out;
+ }
+@@ -260,11 +303,9 @@ CURLcode Curl_ssl_peer_key_make(struct Curl_cfilter *cf,
+ goto out;
+ }
+
+- if(ssl->clientcert && ssl->clientcert[0]) {
+- r = curlx_dyn_add(&buf, ":CCERT");
+- if(r)
+- goto out;
+- }
++ r = cf_ssl_peer_key_add_mtls(&buf, ssl, &is_local);
++ if(r)
++ goto out;
+ #ifdef USE_TLS_SRP
+ if(ssl->username || ssl->password) {
+ r = curlx_dyn_add(&buf, ":SRP-AUTH");
+@@ -295,6 +336,15 @@ out:
+ return r;
+ }
+
++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);
++ return Curl_ssl_peer_key_build(ssl, peer, cf->conn, tls_id, ppeer_key);
++}
++
+ struct Curl_ssl_scache {
+ unsigned int magic;
+ struct Curl_ssl_scache_peer *peers;
+@@ -402,6 +452,7 @@ static void cf_ssl_scache_clear_peer(struct Curl_ssl_scache_peer *peer)
+ }
+ peer->sobj_free = NULL;
+ Curl_safefree(peer->clientcert);
++ Curl_safefree(peer->key_passwd);
+ #ifdef USE_TLS_SRP
+ Curl_safefree(peer->srp_username);
+ Curl_safefree(peer->srp_password);
+@@ -430,8 +481,8 @@ static void cf_ssl_cache_peer_update(struct Curl_ssl_scache_peer *peer)
+ * - 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->exportable = (!peer->clientcert && !peer->key_passwd &&
++ !peer->srp_username && !peer->srp_password &&
+ (!peer->ssl_peer_key ||
+ cf_ssl_peer_key_is_global(peer->ssl_peer_key)));
+ }
+@@ -440,6 +491,7 @@ static CURLcode
+ cf_ssl_scache_peer_init(struct Curl_ssl_scache_peer *peer,
+ const char *ssl_peer_key,
+ const char *clientcert,
++ const char *key_passwd,
+ const char *srp_username,
+ const char *srp_password,
+ const unsigned char *salt,
+@@ -468,6 +520,11 @@ cf_ssl_scache_peer_init(struct Curl_ssl_scache_peer *peer,
+ if(!peer->clientcert)
+ goto out;
+ }
++ if(key_passwd) {
++ peer->key_passwd = curlx_strdup(key_passwd);
++ if(!peer->key_passwd)
++ goto out;
++ }
+ if(srp_username) {
+ peer->srp_username = curlx_strdup(srp_username);
+ if(!peer->srp_username)
+@@ -598,7 +655,7 @@ 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)
++ if(peer->clientcert || peer->key_passwd)
+ return FALSE;
+ #ifdef USE_TLS_SRP
+ if(peer->srp_username || peer->srp_password)
+@@ -608,6 +665,8 @@ static bool cf_ssl_scache_match_auth(struct Curl_ssl_scache_peer *peer,
+ }
+ else if(!Curl_safecmp(peer->clientcert, conn_config->clientcert))
+ return FALSE;
++ if(Curl_timestrcmp(peer->key_passwd, conn_config->key_passwd))
++ return FALSE;
+ #ifdef USE_TLS_SRP
+ if(Curl_timestrcmp(peer->srp_username, conn_config->username) ||
+ Curl_timestrcmp(peer->srp_password, conn_config->password))
+@@ -734,13 +793,21 @@ static CURLcode cf_ssl_add_peer(struct Curl_easy *data,
+
+ peer = cf_ssl_get_free_peer(scache);
+ if(peer) {
++ char buffer[64];
+ const char *ccert = conn_config ? conn_config->clientcert : NULL;
++ const char *kpasswd = conn_config ? conn_config->key_passwd : 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,
++ if(!ccert && conn_config && conn_config->cert_blob) {
++ /* when using a client cert blob, create a name for it */
++ curl_msnprintf(buffer, sizeof(buffer),
++ "cert-%p", conn_config->cert_blob->data);
++ ccert = buffer; /* data is strduped by cf_ssl_scache_peer_init */
++ }
++ result = cf_ssl_scache_peer_init(peer, ssl_peer_key, ccert, kpasswd,
+ username, password, NULL, NULL);
+ if(result)
+ goto out;
+@@ -1115,7 +1182,7 @@ CURLcode Curl_ssl_session_import(struct Curl_easy *data,
+ if(!peer) {
+ peer = cf_ssl_get_free_peer(scache);
+ if(peer) {
+- r = cf_ssl_scache_peer_init(peer, ssl_peer_key, NULL,
++ r = cf_ssl_scache_peer_init(peer, ssl_peer_key, NULL, NULL,
+ NULL, NULL, salt, hmac);
+ if(r)
+ goto out;
+diff --git a/lib/vtls/vtls_scache.h b/lib/vtls/vtls_scache.h
+index b9db0f19b3..d33f2bb1a7 100644
+--- a/lib/vtls/vtls_scache.h
++++ b/lib/vtls/vtls_scache.h
+@@ -66,6 +66,22 @@ CURLcode Curl_ssl_peer_key_make(struct Curl_cfilter *cf,
+ const char *tls_id,
+ char **ppeer_key);
+
++/**
++ * Like Curl_ssl_peer_key_make() but takes the primary config and peer
++ * descriptors directly, without requiring a Curl_cfilter. Exposed for
++ * unit testing.
++ * @param ssl the primary SSL config to key on
++ * @param peer the peer the filter wants to talk to
++ * @param conn connection data for connect-to keying, or NULL
++ * @param tls_id identifier of TLS implementation for sessions
++ * @param ppeer_key on successful return, the key generated
++ */
++CURLcode Curl_ssl_peer_key_build(struct ssl_primary_config *ssl,
++ const struct ssl_peer *peer,
++ const struct connectdata *conn,
++ const char *tls_id,
++ char **ppeer_key);
++
+ /* Return if there is a session cache shall be used.
+ * An ssl session might not be configured or not available for
+ * "connect-only" transfers.
+diff --git a/lib/vtls/wolfssl.c b/lib/vtls/wolfssl.c
+index aa841a754a..74bd247e68 100644
+--- a/lib/vtls/wolfssl.c
++++ b/lib/vtls/wolfssl.c
+@@ -919,10 +919,10 @@ static CURLcode client_certificate(struct Curl_easy *data,
+ #ifndef NO_FILESYSTEM
+ if(ssl_config->primary.cert_blob || ssl_config->primary.clientcert) {
+ const char *cert_file = ssl_config->primary.clientcert;
+- const char *key_file = ssl_config->key;
++ const char *key_file = ssl_config->primary.key;
+ const struct curl_blob *cert_blob = ssl_config->primary.cert_blob;
+- const struct curl_blob *key_blob = ssl_config->key_blob;
+- int file_type = wssl_do_file_type(ssl_config->cert_type);
++ const struct curl_blob *key_blob = ssl_config->primary.key_blob;
++ int file_type = wssl_do_file_type(ssl_config->primary.cert_type);
+ int rc;
+
+ switch(file_type) {
+@@ -953,7 +953,7 @@ static CURLcode client_certificate(struct Curl_easy *data,
+ key_file = cert_file;
+ }
+ else
+- file_type = wssl_do_file_type(ssl_config->key_type);
++ file_type = wssl_do_file_type(ssl_config->primary.key_type);
+
+ rc = key_blob ?
+ wolfSSL_CTX_use_PrivateKey_buffer(wctx->ssl_ctx, key_blob->data,
+@@ -967,8 +967,8 @@ static CURLcode client_certificate(struct Curl_easy *data,
+ #else /* NO_FILESYSTEM */
+ if(ssl_config->primary.cert_blob) {
+ const struct curl_blob *cert_blob = ssl_config->primary.cert_blob;
+- const struct curl_blob *key_blob = ssl_config->key_blob;
+- int file_type = wssl_do_file_type(ssl_config->cert_type);
++ const struct curl_blob *key_blob = ssl_config->primary.key_blob;
++ int file_type = wssl_do_file_type(ssl_config->primary.cert_type);
+ int rc;
+
+ switch(file_type) {
+@@ -993,7 +993,7 @@ static CURLcode client_certificate(struct Curl_easy *data,
+ if(!key_blob)
+ key_blob = cert_blob;
+ else
+- file_type = wssl_do_file_type(ssl_config->key_type);
++ file_type = wssl_do_file_type(ssl_config->primary.key_type);
+
+ if(wolfSSL_CTX_use_PrivateKey_buffer(wctx->ssl_ctx, key_blob->data,
+ (long)key_blob->len,
+diff --git a/tests/data/Makefile.am b/tests/data/Makefile.am
+index 4fd7dfe912..2217666382 100644
+--- a/tests/data/Makefile.am
++++ b/tests/data/Makefile.am
+@@ -282,6 +282,9 @@ test3100 test3101 test3102 test3103 test3104 test3105 \
+ \
+ test3200 test3201 test3202 test3203 test3204 test3205 test3206 test3207 test3208 \
+ test3209 test3210 test3211 test3212 test3213 test3214 test3215 test3216 \
++\
++test3303 test3304 \
++\
+ test4000 test4001
+
+ EXTRA_DIST = $(TESTCASES) DISABLED data-xml1 data320.html \
+diff --git a/tests/data/test3303 b/tests/data/test3303
+new file mode 100644
+index 0000000000..697049f013
+--- /dev/null
++++ b/tests/data/test3303
+@@ -0,0 +1,20 @@
++<?xml version="1.0" encoding="US-ASCII"?>
++<testcase>
++<info>
++<keywords>
++unittest
++TLS
++mTLS
++</keywords>
++</info>
++
++# Client-side
++<client>
++<features>
++unittest
++</features>
++<name>
++conn-reuse match distinguishes mTLS key, cert_type, key_type and key_passwd fields
++</name>
++</client>
++</testcase>
+diff --git a/tests/data/test3304 b/tests/data/test3304
+new file mode 100644
+index 0000000000..4380c0819f
+--- /dev/null
++++ b/tests/data/test3304
+@@ -0,0 +1,20 @@
++<?xml version="1.0" encoding="US-ASCII"?>
++<testcase>
++<info>
++<keywords>
++unittest
++TLS
++mTLS
++</keywords>
++</info>
++
++# Client-side
++<client>
++<features>
++unittest
++</features>
++<name>
++TLS session cache peer key discriminates on mTLS key, key_type and cert_type fields
++</name>
++</client>
++</testcase>
+diff --git a/tests/unit/Makefile.inc b/tests/unit/Makefile.inc
+index 149636cbcc..bba30f6706 100644
+--- a/tests/unit/Makefile.inc
++++ b/tests/unit/Makefile.inc
+@@ -43,4 +43,5 @@ TESTS_C = \
+ unit1979.c unit1980.c \
+ unit2600.c unit2601.c unit2602.c unit2603.c unit2604.c unit2605.c \
+ unit3200.c unit3205.c \
+- unit3211.c unit3212.c unit3213.c unit3214.c unit3216.c
++ unit3211.c unit3212.c unit3213.c unit3214.c unit3216.c \
++ unit3303.c unit3304.c
+diff --git a/tests/unit/unit3303.c b/tests/unit/unit3303.c
+new file mode 100644
+index 0000000000..41bced542d
+--- /dev/null
++++ b/tests/unit/unit3303.c
+@@ -0,0 +1,127 @@
++/***************************************************************************
++ * _ _ ____ _
++ * 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 "unitcheck.h"
++#include "urldata.h"
++
++#ifdef USE_SSL
++#include "vtls/vtls.h"
++#endif
++
++static CURLcode test_unit3303(const char *arg)
++{
++ UNITTEST_BEGIN_SIMPLE
++
++#ifdef USE_SSL
++ {
++ CURL *curl;
++ struct connectdata *conn;
++ struct ssl_primary_config *primary;
++ char *saved;
++ static char alt_passwd[] = "wrong";
++ static char alt_key[] = "other.key";
++ static char alt_ktype[] = "DER";
++ static char alt_ctype[] = "P12";
++
++ curl_global_init(CURL_GLOBAL_ALL);
++ curl = curl_easy_init();
++ if(!curl) {
++ curl_global_cleanup();
++ goto unit_test_abort;
++ }
++
++ curl_easy_setopt(curl, CURLOPT_SSLCERT, "client.pem");
++ curl_easy_setopt(curl, CURLOPT_SSLKEY, "client.key");
++ curl_easy_setopt(curl, CURLOPT_KEYPASSWD, "secret");
++ curl_easy_setopt(curl, CURLOPT_SSLCERTTYPE, "PEM");
++ curl_easy_setopt(curl, CURLOPT_SSLKEYTYPE, "PEM");
++
++ if(Curl_ssl_easy_config_complete((struct Curl_easy *)curl)) {
++ curl_easy_cleanup(curl);
++ curl_global_cleanup();
++ goto unit_test_abort;
++ }
++
++ conn = curlx_calloc(1, sizeof(*conn));
++ if(!conn || Curl_ssl_conn_config_init((struct Curl_easy *)curl, conn)) {
++ if(conn)
++ Curl_ssl_conn_config_cleanup(conn);
++ curlx_free(conn);
++ curl_easy_cleanup(curl);
++ curl_global_cleanup();
++ goto unit_test_abort;
++ }
++
++ /* Baseline: identical config must match. */
++ fail_unless(Curl_ssl_conn_config_match((struct Curl_easy *)curl, conn,
++ FALSE),
++ "identical mTLS config should match");
++
++ primary = &((struct Curl_easy *)curl)->set.ssl.primary;
++
++ /* Different key_passwd must not match. */
++ saved = primary->key_passwd;
++ primary->key_passwd = alt_passwd;
++ fail_unless(!Curl_ssl_conn_config_match((struct Curl_easy *)curl, conn,
++ FALSE),
++ "different key_passwd must not reuse conn");
++ primary->key_passwd = saved;
++
++ /* Different key path must not match. */
++ saved = primary->key;
++ primary->key = alt_key;
++ fail_unless(!Curl_ssl_conn_config_match((struct Curl_easy *)curl, conn,
++ FALSE),
++ "different key must not reuse conn");
++ primary->key = saved;
++
++ /* Different key type must not match. */
++ saved = primary->key_type;
++ primary->key_type = alt_ktype;
++ fail_unless(!Curl_ssl_conn_config_match((struct Curl_easy *)curl, conn,
++ FALSE),
++ "different key_type must not reuse conn");
++ primary->key_type = saved;
++
++ /* Different cert type must not match. */
++ saved = primary->cert_type;
++ primary->cert_type = alt_ctype;
++ fail_unless(!Curl_ssl_conn_config_match((struct Curl_easy *)curl, conn,
++ FALSE),
++ "different cert_type must not reuse conn");
++ primary->cert_type = saved;
++
++ /* All fields restored: must match again. */
++ fail_unless(Curl_ssl_conn_config_match((struct Curl_easy *)curl, conn,
++ FALSE),
++ "restored mTLS config should match");
++
++ Curl_ssl_conn_config_cleanup(conn);
++ curlx_free(conn);
++ curl_easy_cleanup(curl);
++ curl_global_cleanup();
++ }
++#endif /* USE_SSL */
++
++ UNITTEST_END_SIMPLE
++}
+diff --git a/tests/unit/unit3304.c b/tests/unit/unit3304.c
+new file mode 100644
+index 0000000000..4defa1e426
+--- /dev/null
++++ b/tests/unit/unit3304.c
+@@ -0,0 +1,163 @@
++/***************************************************************************
++ * _ _ ____ _
++ * 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
++ *
++ ***************************************************************************/
++
++/* Unit tests for TLS session cache peer key discrimination on mTLS fields.
++ * Verifies that Curl_ssl_peer_key_build() produces distinct keys when two
++ * handles differ only on key, key_type or cert_type. key_passwd is NOT
++ * embedded in the peer key; it is compared separately at session lookup via
++ * cf_ssl_scache_match_auth(), following the same pattern as SRP
++ * credentials. */
++
++#include "unitcheck.h"
++#include "urldata.h"
++
++#ifdef USE_SSL
++#include "vtls/vtls.h"
++#include "vtls/vtls_scache.h"
++#endif
++
++static CURLcode test_unit3304(const char *arg)
++{
++ UNITTEST_BEGIN_SIMPLE
++
++#ifdef USE_SSL
++ {
++ struct ssl_peer peer;
++ struct ssl_primary_config ssl;
++ char *key1 = NULL;
++ char *key2 = NULL;
++ static char base_hostname[] = "example.com";
++ static char base_cert[] = "client.pem";
++ static char base_key[] = "client.key";
++ static char base_passwd[] = "secret";
++ static char base_ctype[] = "PEM";
++ static char base_ktype[] = "PEM";
++ static char alt_key[] = "other.key";
++ static char alt_ktype[] = "DER";
++ static char alt_ctype[] = "P12";
++ static char lc_ctype[] = "pem";
++ static char lc_ktype[] = "pem";
++
++ memset(&peer, 0, sizeof(peer));
++ peer.hostname = base_hostname;
++ peer.port = 443;
++ peer.transport = TRNSPRT_TCP;
++
++ memset(&ssl, 0, sizeof(ssl));
++ ssl.verifypeer = TRUE;
++ ssl.verifyhost = TRUE;
++ ssl.clientcert = base_cert;
++ ssl.key = base_key;
++ ssl.key_passwd = base_passwd;
++ ssl.cert_type = base_ctype;
++ ssl.key_type = base_ktype;
++
++ /* Baseline: same config produces same key. */
++ fail_unless(!Curl_ssl_peer_key_build(&ssl, &peer, NULL, "test", &key1),
++ "peer key build failed");
++ fail_unless(!Curl_ssl_peer_key_build(&ssl, &peer, NULL, "test", &key2),
++ "peer key build failed");
++ fail_unless(key1 && key2 && !strcmp(key1, key2),
++ "identical config should produce identical peer key");
++ curlx_free(key1); key1 = NULL;
++ curlx_free(key2); key2 = NULL;
++
++ /* key_passwd is NOT in the peer key: lookup uses timing-safe comparison
++ * via cf_ssl_scache_match_auth(), same as SRP credentials. */
++ fail_unless(!Curl_ssl_peer_key_build(&ssl, &peer, NULL, "test", &key1),
++ "peer key build failed");
++ ssl.key_passwd = NULL;
++ fail_unless(!Curl_ssl_peer_key_build(&ssl, &peer, NULL, "test", &key2),
++ "peer key build failed");
++ fail_unless(key1 && key2 && !strcmp(key1, key2),
++ "key_passwd must not affect the peer key");
++ curlx_free(key1); key1 = NULL;
++ curlx_free(key2); key2 = NULL;
++ ssl.key_passwd = base_passwd;
++
++ /* Different key path must produce a different peer key. */
++ fail_unless(!Curl_ssl_peer_key_build(&ssl, &peer, NULL, "test", &key1),
++ "peer key build failed");
++ ssl.key = alt_key;
++ fail_unless(!Curl_ssl_peer_key_build(&ssl, &peer, NULL, "test", &key2),
++ "peer key build failed");
++ fail_unless(key1 && key2 && strcmp(key1, key2),
++ "different key must produce different peer key");
++ curlx_free(key1); key1 = NULL;
++ curlx_free(key2); key2 = NULL;
++ ssl.key = base_key;
++
++ /* Different key_type must produce a different peer key. */
++ fail_unless(!Curl_ssl_peer_key_build(&ssl, &peer, NULL, "test", &key1),
++ "peer key build failed");
++ ssl.key_type = alt_ktype;
++ fail_unless(!Curl_ssl_peer_key_build(&ssl, &peer, NULL, "test", &key2),
++ "peer key build failed");
++ fail_unless(key1 && key2 && strcmp(key1, key2),
++ "different key_type must produce different peer key");
++ curlx_free(key1); key1 = NULL;
++ curlx_free(key2); key2 = NULL;
++ ssl.key_type = base_ktype;
++
++ /* Different cert_type must produce a different peer key. */
++ fail_unless(!Curl_ssl_peer_key_build(&ssl, &peer, NULL, "test", &key1),
++ "peer key build failed");
++ ssl.cert_type = alt_ctype;
++ fail_unless(!Curl_ssl_peer_key_build(&ssl, &peer, NULL, "test", &key2),
++ "peer key build failed");
++ fail_unless(key1 && key2 && strcmp(key1, key2),
++ "different cert_type must produce different peer key");
++ curlx_free(key1); key1 = NULL;
++ curlx_free(key2); key2 = NULL;
++ ssl.cert_type = base_ctype;
++
++ /* cert_type is case-insensitive: "PEM" and "pem" must produce the
++ * same peer key, consistent with the conn-reuse comparison. */
++ fail_unless(!Curl_ssl_peer_key_build(&ssl, &peer, NULL, "test", &key1),
++ "peer key build failed");
++ ssl.cert_type = lc_ctype;
++ fail_unless(!Curl_ssl_peer_key_build(&ssl, &peer, NULL, "test", &key2),
++ "peer key build failed");
++ fail_unless(key1 && key2 && !strcmp(key1, key2),
++ "cert_type case must not affect peer key");
++ curlx_free(key1); key1 = NULL;
++ curlx_free(key2); key2 = NULL;
++ ssl.cert_type = base_ctype;
++
++ /* key_type is case-insensitive: "PEM" and "pem" must produce the
++ * same peer key. */
++ fail_unless(!Curl_ssl_peer_key_build(&ssl, &peer, NULL, "test", &key1),
++ "peer key build failed");
++ ssl.key_type = lc_ktype;
++ fail_unless(!Curl_ssl_peer_key_build(&ssl, &peer, NULL, "test", &key2),
++ "peer key build failed");
++ fail_unless(key1 && key2 && !strcmp(key1, key2),
++ "key_type case must not affect peer key");
++ curlx_free(key1); key1 = NULL;
++ curlx_free(key2); key2 = NULL;
++ }
++#endif /* USE_SSL */
++
++ UNITTEST_END_SIMPLE
++}
+--
+2.35.6
@@ -25,6 +25,8 @@ SRC_URI = " \
file://CVE-2026-4873.patch \
file://CVE-2026-8286.patch \
file://CVE-2026-8927.patch \
+ file://CVE-2026-8932-dependent.patch \
+ file://CVE-2026-8932.patch \
"
SRC_URI:append:class-nativesdk = " \