<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Performance Deep]]></title><description><![CDATA[Performance Deep]]></description><link>https://perf-deep.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Fri, 18 Sep 2026 08:01:31 GMT</lastBuildDate><atom:link href="https://perf-deep.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Debugging mTLS in Gatling: The Silent Failure That Took Hours to Crack]]></title><description><![CDATA[A certificate was loaded. Java said nothing. The server just closed the door.

I was performance testing a REST API that required mutual TLS (mTLS) authentication. The setup looked correct — .pfx cert]]></description><link>https://perf-deep.hashnode.dev/debugging-mtls-in-gatling-the-silent-failure-that-took-hours-to-crack</link><guid isPermaLink="true">https://perf-deep.hashnode.dev/debugging-mtls-in-gatling-the-silent-failure-that-took-hours-to-crack</guid><category><![CDATA[Gatling]]></category><category><![CDATA[mTLS]]></category><category><![CDATA[TLS]]></category><category><![CDATA[premature-close]]></category><dc:creator><![CDATA[Niral Patelia]]></dc:creator><pubDate>Mon, 11 May 2026 13:45:13 GMT</pubDate><content:encoded><![CDATA[<blockquote>
<p>A certificate was loaded. Java said nothing. The server just closed the door.</p>
</blockquote>
<p>I was performance testing a REST API that required mutual TLS (mTLS) authentication. The setup looked correct — <code>.pfx</code> certificate loaded, passphrase configured, <code>KeyManagerFactory</code> wired up, simulation compiled cleanly.</p>
<p>Then I hit run. Every single request failed. No useful error message. Just:</p>
<pre><code class="language-shell">j.i.IOException: Premature close
</code></pre>
<p>This is the story of what went wrong, why Java stayed silent about it, and exactly how to fix it.</p>
<h3>First, What Is mTLS?</h3>
<p>Before diving into the bug, it helps to understand the difference between standard TLS and mutual TLS.</p>
<p>In <strong>standard HTTPS (one-way TLS)</strong>, only the server proves its identity. The browser or client receives the server's certificate, verifies it against trusted certificate authorities, and proceeds. The server never checks who the client is.</p>
<p>In <strong>mutual TLS (mTLS)</strong>, both sides prove their identity to each other:</p>
<pre><code class="language-javascriptreact">Client  ──── presents its certificate ────►  Server

Client  ◄─── presents its certificate ────  Server

           Both verify before proceeding
</code></pre>
<p>mTLS is common in internal microservice communication, financial APIs, B2B integrations, and anywhere the server needs cryptographic proof of <em>which</em> client is calling — not just that a valid TLS connection exists.</p>
<h3>The Setup: Loading a PFX Certificate in Gatling</h3>
<p>A <code>.pfx</code> (also called PKCS#12) file bundles a private key and certificate chain into a single password-protected file. To use it in Gatling, the standard approach is:</p>
<pre><code class="language-java">val sslContext: SSLContext = {
  val keyStore = KeyStore.getInstance("PKCS12")
  val pfxStream = getClass.getResourceAsStream("/certs/client.pfx")
  keyStore.load(pfxStream, "your-passphrase".toCharArray)

  val kmf = KeyManagerFactory.getInstance(
    KeyManagerFactory.getDefaultAlgorithm
  )
  kmf.init(keyStore, "your-passphrase".toCharArray)

  val ctx = SSLContext.getInstance("TLS")
  ctx.init(kmf.getKeyManagers, null, null)
  ctx
}
</code></pre>
<p>This compiled. The simulation ran. And every request returned:</p>
<pre><code class="language-shell">j.i.IOException: Premature close
</code></pre>
<h3>Diagnosing "Premature Close"</h3>
<p>"Premature close" means the TCP connection was abruptly shut down by the remote end before a complete HTTP response was received. It is not a TLS error. It is not a certificate error. It is the aftermath of something going wrong at a lower level that Java never surfaced clearly.</p>
<p>To investigate, I enabled TLS debug logging:</p>
<pre><code class="language-plaintext">-Djavax.net.debug=ssl:handshake
</code></pre>
<p>In the output, I could see:</p>
<ol>
<li><p>Client Hello sent ✅</p>
</li>
<li><p>Server Hello received ✅</p>
</li>
<li><p>Server sent CertificateRequest ✅</p>
</li>
<li><p>Client sent <strong>empty certificate</strong> — no certificate at all ❌</p>
</li>
<li><p>Server closed the connection 🛑</p>
</li>
</ol>
<p>Step 4 was the smoking gun. Java received the server's CertificateRequest and responded with nothing.</p>
<h3>The Real Root Cause: Extended Key Usage</h3>
<p>Every X.509 certificate contains an <strong>Extended Key Usages (EKU)</strong> extension. This field is an array that declares what the certificate is permitted to be used for. The two most relevant values are:</p>
<table>
<thead>
<tr>
<th>OID</th>
<th>Value</th>
<th>Meaning</th>
</tr>
</thead>
<tbody><tr>
<td>1.3.6.1.5.5.7.3.1</td>
<td>serverAuth</td>
<td>Identifies a server</td>
</tr>
<tr>
<td>1.3.6.1.5.5.7.3.2</td>
<td>clientAuth</td>
<td>Identifies a client</td>
</tr>
</tbody></table>
<p>You can inspect a certificate's EKU with:</p>
<pre><code class="language-shell">openssl pkcs12 -in cert.pfx -nokeys -passin pass:yourpassphrase | \\
  openssl x509 -noout -text | grep -A2 "Extended Key Usage"
</code></pre>
<p>When I ran this, the output was:</p>
<pre><code class="language-shell">X509v3 Extended Key Usage:
    TLS Web Server Authentication
</code></pre>
<p>There it was. The certificate had <strong>serverAuth</strong>, not <strong>clientAuth</strong>.</p>
<p>Java's default `X509KeyManager` — the class responsible for choosing which certificate to send during a TLS handshake — filters candidates by their Extended Key Usage. When the server issues a CertificateRequest, Java evaluates every alias in the keystore and asks: <em>"Is this certificate valid for client authentication?"</em></p>
<p>My certificate said: <em>"I am a server certificate."</em></p>
<p>Java's response: silently skip it. Send nothing.</p>
<p>No warning. No exception. No log line saying, "no suitable certificate found." Just silence — and then a broken connection on the other end.</p>
<h3>Why KeyManagerFactory Alone Isn't Enough</h3>
<p>When you initialize a <code>KeyManagerFactory</code> and call <code>getKeyManagers()</code>, you get back a default <code>X509KeyManager</code> implementation. This implementation has a method called <code>chooseClientAlias</code> that is called during the TLS handshake to select which certificate alias to present.</p>
<p>Here is what the default implementation does internally:</p>
<ol>
<li><p>Gets the list of acceptable CAs from the server's CertificateRequest</p>
</li>
<li><p>Iterates through every alias in the keystore</p>
</li>
<li><p>For each alias, checks if the certificate chain is signed by an acceptable CA</p>
</li>
<li><p><strong>Checks the Extended Key Usage — filters out any cert without clientAuth</strong></p>
</li>
<li><p>Returns the first matching alias, or null if none match</p>
</li>
</ol>
<p>Step 4 is where everything failed. The certificate was loaded correctly. The passphrase was correct. The keystore had exactly one alias. But the EKU check returned null, Java sent an empty certificate message, and the server closed the connection.</p>
<h3>The Fix: A Custom X509KeyManager</h3>
<p>The correct long-term fix is to request a new certificate from your certificate authority with <code>clientAuth</code> in the Extended Key Usage. If you control the PKI, do that.</p>
<p>But in many real-world scenarios — third-party integrations, legacy systems, certificates issued by external organizations — you cannot simply request a new cert. You need to work with what you have.</p>
<p>The solution is to wrap the default <code>X509KeyManager</code> in a custom implementation that overrides <code>chooseClientAlias</code> to bypass the EKU check entirely:</p>
<pre><code class="language-java">import java.net.Socket
import java.security.Principal
import java.security.cert.X509Certificate
import javax.net.ssl.{X509KeyManager, KeyManagerFactory, SSLContext}

// Step 1: Define the alias of your certificate in the keystore
private val keyAlias = "cert.pfx certificate"

// Step 2: Create a custom KeyManager that forces the alias
class ForcedAliasKeyManager(
    delegate: X509KeyManager,
    alias: String
) extends X509KeyManager {

  // THE KEY OVERRIDE: When server asks for a client cert,
  // always return our alias — bypassing the EKU check
  override def chooseClientAlias(
      keyType: Array[String],
      issuers: Array[Principal],
      socket: Socket): String = alias

  // Delegate everything else to the default implementation
  override def getClientAliases(
      keyType: String,
      issuers: Array[Principal]): Array[String] =
    delegate.getClientAliases(keyType, issuers)

  override def getPrivateKey(alias: String) =
    delegate.getPrivateKey(alias)

  override def getCertificateChain(alias: String) =
    delegate.getCertificateChain(alias)

  // Server-side methods — not used in client context
  override def chooseServerAlias(
      keyType: String,
      issuers: Array[Principal],
      socket: Socket): String =
    delegate.chooseServerAlias(keyType, issuers, socket)

  override def getServerAliases(
      keyType: String,
      issuers: Array[Principal]): Array[String] =
    delegate.getServerAliases(keyType, issuers)
}

// Step 3: Wire it into the SSLContext
val sslContext: SSLContext = {
  val keyStore = KeyStore.getInstance("PKCS12")
  val pfxStream = getClass.getResourceAsStream("/certs/client.pfx")
  keyStore.load(pfxStream, "your-passphrase".toCharArray)

  val kmf = KeyManagerFactory.getInstance(
    KeyManagerFactory.getDefaultAlgorithm
  )
  kmf.init(keyStore, "your-passphrase".toCharArray)

  // Wrap the default key manager with our forced alias version
  val defaultKM = kmf.getKeyManagers.head.asInstanceOf[X509KeyManager]
  val forcedKM = new ForcedAliasKeyManager(defaultKM, keyAlias)

  val ctx = SSLContext.getInstance("TLS")
  ctx.init(Array(forcedKM), null, null)
  ctx
}
</code></pre>
<h3>Why This Works</h3>
<p>The <code>chooseClientAlias</code> method is the single decision point in the TLS handshake where Java selects which certificate to present. By overriding it to unconditionally return our alias, we short-circuit the entire EKU filtering logic.</p>
<p>Java no longer asks "is this certificate valid for client auth?" — it just uses the one we tell it to.</p>
<p>All other methods — <code>getPrivateKey</code>, <code>getCertificateChain</code>— are delegated to the original key manager, which handles them correctly using the keystore we loaded.</p>
<p>The complete TLS handshake now looks like:</p>
<pre><code class="language-javascriptreact">Server → CertificateRequest

Java's default KeyManager:
  checks EKU → serverAuth → no clientAuth → returns null → sends empty cert

Our ForcedAliasKeyManager:
  ignores EKU → returns alias → sends certificate ✓

Server receives cert → verifies chain → handshake completes ✓
Requests succeed ✓
</code></pre>
<h3>Finding the Alias Name</h3>
<p>One detail worth noting: the alias name in the keystore matters. To find the exact alias of your certificate:</p>
<pre><code class="language-shell">keytool -list -v -keystore cert.pfx -storetype PKCS12 -storepass yourpassphrase
</code></pre>
<p>Look for the <code>Alias name</code> field in the output. That string is what you pass to <code>ForcedAliasKeyManager</code>.</p>
<h3>Summary</h3>
<table>
<thead>
<tr>
<th></th>
<th>Detail</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Error</strong></td>
<td>j.i.IOException: Premature close</td>
</tr>
<tr>
<td><strong>Real cause</strong></td>
<td>Certificate EKU was serverAuth, not clientAuth</td>
</tr>
<tr>
<td><strong>Java behavior</strong></td>
<td>Silently skipped the cert, sent empty CertificateRequest response</td>
</tr>
<tr>
<td><strong>Proper fix</strong></td>
<td>Reissue certificate with clientAuth EKU</td>
</tr>
<tr>
<td><strong>Workaround</strong></td>
<td>Custom X509KeyManager with forced alias selection</td>
</tr>
<tr>
<td><strong>Key method</strong></td>
<td>Override chooseClientAlias to bypass EKU filtering</td>
</tr>
</tbody></table>
<h3>Key Takeaways</h3>
<ol>
<li><p><strong>"Premature close" in mTLS is almost never what it looks like.</strong> The connection wasn't terminated randomly — the server closed it because Java sent no certificate.</p>
</li>
<li><p><strong>Java's X509KeyManager fails silently.</strong> There is no exception or log when it finds no suitable certificate. Enable `-Djavax.net.debug=ssl:handshake` to see the actual handshake.</p>
</li>
<li><p><strong>EKU is enforced by the client, not the server.</strong> The server asked for a certificate. Java refused to send one based on the EKU field. The server never even got to evaluate the certificate's validity.</p>
</li>
<li><p><strong>A custom KeyManager is a legitimate escape hatch.</strong> When you cannot control certificate issuance, overriding `chooseClientAlias` is a clean, contained workaround with minimal surface area.</p>
</li>
<li><p><strong>Always inspect your certificate before debugging the code.</strong> One `openssl` command would have found this in minutes.</p>
</li>
</ol>
<p><em>If this helped you, follow me for more performance engineering and backend debugging deep-dives.</em></p>
]]></content:encoded></item></channel></rss>