Commit 54dacb1710 for openssl.org

commit 54dacb1710cf0dd00d7c6f2c4c7ee7a19d5028a4
Author: Neil Horman <nhorman@openssl.org>
Date:   Thu May 14 11:39:05 2026 -0400

    Cache decoded certs on client in CTX to avoid multiple asn1 parsing

    our TLS client path has to do an ASN1 decode on every new connection it
    establishes.  Other TLS stacks (notably rustls) seems to not do this
    decode, opting instead to to do just enough der parsing to verify the
    certificate chain.

    We can't really do that, but we can maybe do some things to speed up
    this path.  Most notably we can cache the received certificate der
    string in the SSL_CTX along with a previously decoded X509 certificate
    to avoid the overhead of additional ASN1 parsings in future connections

    Reviewed-by: Bob Beck <beck@openssl.org>
    Reviewed-by: Nikola Pajkovsky <nikolap@openssl.org>
    Merge-date: Wed Oct 21 16:44:29 2026
    Merged-from: https://github.com/openssl/openssl/pull/33085

diff --git a/ssl/ssl_lib.c b/ssl/ssl_lib.c
index ff039c7138..36f506dfe5 100644
--- a/ssl/ssl_lib.c
+++ b/ssl/ssl_lib.c
@@ -4432,6 +4432,9 @@ SSL_CTX *SSL_CTX_new_ex(OSSL_LIB_CTX *libctx, const char *propq,
         goto err;
     if ((ret->tktenc = EVP_CIPHER_fetch(libctx, "AES-256-CBC", propq)) == NULL)
         goto err;
+    if ((ret->sha1 = EVP_MD_fetch(libctx, "SHA1", propq)) == NULL)
+        goto err;
+
 #if defined(OPENSSL_HAVE_TLS1PRF)
     if ((ret->tls1prf = EVP_KDF_fetch(libctx, OSSL_KDF_NAME_TLS1_PRF, propq)) == NULL)
         goto err;
@@ -4458,6 +4461,14 @@ SSL_CTX *SSL_CTX_new_ex(OSSL_LIB_CTX *libctx, const char *propq,
         ERR_raise(ERR_LIB_SSL, ERR_R_X509_LIB);
         goto err;
     }
+
+    if (!SSL_CTX_is_server(ret)) {
+        ret->handshake_certs = sk_X509_HS_CACHE_ENT_new(NULL);
+        if (ret->handshake_certs == NULL) {
+            ERR_raise(ERR_LIB_SSL, ERR_R_CRYPTO_LIB);
+            goto err;
+        }
+    }
 #ifndef OPENSSL_NO_CT
     ret->ctlog_store = CTLOG_STORE_new_ex(libctx, propq);
     if (ret->ctlog_store == NULL) {
@@ -4717,6 +4728,13 @@ int SSL_CTX_up_ref(SSL_CTX *ctx)
     return ((i > 1) ? 1 : 0);
 }

+static void free_hs_cache_ent(X509_HS_CACHE_ENT *entry)
+{
+    X509_free(entry->cert);
+    OPENSSL_free(entry);
+    return;
+}
+
 void SSL_CTX_free(SSL_CTX *a)
 {
     int i;
@@ -4758,6 +4776,7 @@ void SSL_CTX_free(SSL_CTX *a)
     EVP_MAC_free(a->hmac);
     EVP_MD_free(a->sha256);
     EVP_CIPHER_free(a->tktenc);
+    EVP_MD_free(a->sha1);
 #ifdef OPENSSL_HAVE_TLS1PRF
     EVP_KDF_free(a->tls1prf);
 #endif
@@ -4819,6 +4838,7 @@ void SSL_CTX_free(SSL_CTX *a)
     OPENSSL_free(a->client_cert_type);
     OPENSSL_free(a->server_cert_type);

+    sk_X509_HS_CACHE_ENT_pop_free(a->handshake_certs, free_hs_cache_ent);
     CRYPTO_THREAD_lock_free(a->lock);
     CRYPTO_FREE_REF(&a->references);
 #ifdef TSAN_REQUIRES_LOCKING
@@ -4841,6 +4861,93 @@ void SSL_CTX_free(SSL_CTX *a)
     OPENSSL_free(a);
 }

+X509 *ssl_ctx_find_handshake_cert(SSL_CTX *sctx, const unsigned char *certbytes,
+    size_t cert_len, unsigned char *sha1_hash)
+{
+    int idx;
+    X509_HS_CACHE_ENT *find = NULL;
+    X509 *ret = NULL;
+    unsigned int len = SHA_DIGEST_LENGTH;
+    int cache_len;
+
+    if (sctx->handshake_certs == NULL)
+        return NULL;
+
+    if (!EVP_Digest(certbytes, cert_len, sha1_hash, &len, sctx->sha1, NULL))
+        return NULL;
+
+    if (len != SHA_DIGEST_LENGTH)
+        return NULL;
+
+    if (!CRYPTO_THREAD_read_lock(sctx->lock))
+        return NULL;
+
+    cache_len = sk_X509_HS_CACHE_ENT_num(sctx->handshake_certs);
+    for (idx = 0; idx < cache_len; idx++) {
+        find = sk_X509_HS_CACHE_ENT_value(sctx->handshake_certs, idx);
+        if (!memcmp(find->sha1_hash, sha1_hash, SHA_DIGEST_LENGTH)) {
+            X509_up_ref(find->cert);
+            ret = find->cert;
+            break;
+        }
+    }
+    CRYPTO_THREAD_unlock(sctx->lock);
+    return ret;
+}
+
+X509 *ssl_ctx_add_handshake_cert(SSL_CTX *sctx, X509 *cert,
+    unsigned char *sha1_hash)
+{
+    X509_HS_CACHE_ENT *tmp, *del;
+    X509 *ret = cert;
+
+    if (sctx->handshake_certs == NULL)
+        return cert;
+
+    /*
+     * Force extension caching on the X509 now, while we still "own" it
+     * exclusively. From this point on the cached cert is treated as
+     * immutable by all readers (they only X509_up_ref it).
+     */
+    if (X509_check_purpose(cert, -1, 0) <= 0)
+        return cert;
+
+    tmp = OPENSSL_malloc(sizeof(X509_HS_CACHE_ENT));
+    if (tmp == NULL)
+        return cert;
+
+    /*
+     * We had to compute the hash of the der string in the find routine, so
+     * we can reuse it here
+     */
+    memcpy(tmp->sha1_hash, sha1_hash, SHA_DIGEST_LENGTH);
+    tmp->cert = cert;
+    X509_up_ref(cert);
+
+    if (!CRYPTO_THREAD_write_lock(sctx->lock)) {
+        free_hs_cache_ent(tmp);
+        return cert;
+    }
+
+    if (sk_X509_HS_CACHE_ENT_num(sctx->handshake_certs) > 64) {
+        /*
+         * Evict the oldest entry (index 0). Deleting index 64 with
+         * num > 64 would remove the most-recently-pushed entry, which
+         * is the opposite of what we want.
+         */
+        del = sk_X509_HS_CACHE_ENT_delete(sctx->handshake_certs, 0);
+        if (del != NULL)
+            free_hs_cache_ent(del);
+    }
+    if (!sk_X509_HS_CACHE_ENT_push(sctx->handshake_certs, tmp)) {
+        free_hs_cache_ent(tmp);
+        ret = cert;
+    }
+
+    CRYPTO_THREAD_unlock(sctx->lock);
+    return ret;
+}
+
 void SSL_CTX_set_default_passwd_cb(SSL_CTX *ctx, pem_password_cb *cb)
 {
     ctx->default_passwd_callback = cb;
diff --git a/ssl/ssl_local.h b/ssl/ssl_local.h
index 6ba7c54911..283065640c 100644
--- a/ssl/ssl_local.h
+++ b/ssl/ssl_local.h
@@ -866,6 +866,13 @@ typedef struct {
 #define OPENSSL_HAVE_TLS1PRF
 #endif

+typedef struct x509_hs_cache_entry_st {
+    unsigned char sha1_hash[SHA_DIGEST_LENGTH];
+    X509 *cert;
+} X509_HS_CACHE_ENT;
+
+DEFINE_STACK_OF(X509_HS_CACHE_ENT)
+
 struct ssl_ctx_st {
     OSSL_LIB_CTX *libctx;

@@ -877,6 +884,7 @@ struct ssl_ctx_st {
     STACK_OF(SSL_CIPHER) *tls13_ciphersuites;
     struct x509_store_st /* X509_STORE */ *cert_store;
     LHASH_OF(SSL_SESSION) *sessions;
+    STACK_OF(X509_HS_CACHE_ENT) *handshake_certs;
     EVP_MAC *hmac;
     EVP_MD *sha256;
     EVP_CIPHER *tktenc;
@@ -976,6 +984,8 @@ struct ssl_ctx_st {

     CRYPTO_EX_DATA ex_data;

+    EVP_MD *sha1; /* For SSLv3/TLSv1 'ssl3-sha1' */
+
     STACK_OF(X509) *extra_certs;
     STACK_OF(SSL_COMP) *comp_methods; /* stack of SSL_COMP, TLSv1 */

@@ -1274,6 +1284,10 @@ struct ssl_ctx_st {
 #endif
 };

+X509 *ssl_ctx_find_handshake_cert(SSL_CTX *sctx, const unsigned char *certbytes,
+    size_t cert_len, unsigned char *sha1_hash);
+X509 *ssl_ctx_add_handshake_cert(SSL_CTX *sctx, X509 *cert, unsigned char *sha1_hash);
+
 typedef struct ossl_quic_tls_callbacks_st {
     int (*crypto_send_cb)(SSL *s, const unsigned char *buf, size_t buf_len,
         size_t *consumed, void *arg);
diff --git a/ssl/statem/statem_clnt.c b/ssl/statem/statem_clnt.c
index cc291b1ff8..c79d914c76 100644
--- a/ssl/statem/statem_clnt.c
+++ b/ssl/statem/statem_clnt.c
@@ -2510,6 +2510,7 @@ MSG_PROCESS_RETURN tls_process_server_certificate(SSL_CONNECTION *s,
     const unsigned char *certstart, *certbytes;
     size_t chainidx;
     unsigned int context = 0;
+    unsigned char sha1_hash[SHA_DIGEST_LENGTH];
     SSL_CTX *sctx = SSL_CONNECTION_GET_CTX(s);

     if (s->ext.server_cert_type == TLSEXT_cert_type_rpk)
@@ -2541,21 +2542,29 @@ MSG_PROCESS_RETURN tls_process_server_certificate(SSL_CONNECTION *s,
         }

         certstart = certbytes;
-        x = X509_new_ex(sctx->libctx, sctx->propq);
+        x = ssl_ctx_find_handshake_cert(sctx, certstart, cert_len, sha1_hash);
         if (x == NULL) {
-            SSLfatal(s, SSL_AD_DECODE_ERROR, ERR_R_ASN1_LIB);
-            goto err;
-        }
-        if (d2i_X509(&x, (const unsigned char **)&certbytes,
-                cert_len)
-            == NULL) {
-            SSLfatal(s, SSL_AD_BAD_CERTIFICATE, ERR_R_ASN1_LIB);
-            goto err;
-        }
+            x = X509_new_ex(sctx->libctx, sctx->propq);
+            if (x == NULL) {
+                SSLfatal(s, SSL_AD_DECODE_ERROR, ERR_R_ASN1_LIB);
+                goto err;
+            }
+            if (d2i_X509(&x, (const unsigned char **)&certbytes,
+                    cert_len)
+                == NULL) {
+                SSLfatal(s, SSL_AD_BAD_CERTIFICATE, ERR_R_ASN1_LIB);
+                goto err;
+            }

-        if (certbytes != (certstart + cert_len)) {
-            SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_CERT_LENGTH_MISMATCH);
-            goto err;
+            if (certbytes != (certstart + cert_len)) {
+                SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_CERT_LENGTH_MISMATCH);
+                goto err;
+            }
+
+            /*
+             * Add the cert to our handshake cache
+             */
+            x = ssl_ctx_add_handshake_cert(sctx, x, sha1_hash);
         }

         if (SSL_CONNECTION_IS_VERSION13(s)) {