Certificate Authentication Is Harder Than It Looks
X.509 certificate authentication, commonly known as mutual TLS (mTLS), has become a popular choice for zero-trust networks. It offers real advantages over passwords and tokens, but those benefits come with added complexity that often introduces subtle vulnerabilities. This article examines how implementation mistakes in mTLS systems can enable user impersonation, privilege escalation, and information disclosure, based on research presented at Black Hat USA and DEF CON 2023.
How mTLS Actually Works
While most people recognize server certificate validation from the padlock icon in web browsers, the same cryptographic technology can authenticate clients. TLS supports client verification through public and private key cryptography, which happens during the handshake before any application data is exchanged:
When configured for mTLS, a server can request an X.509 certificate from the client. This binary data structure contains identifying information about the client, including its name, public key, and issuer:
$ openssl x509 -text -in client.crt
Certificate:
Data:
Version: 1 (0x0)
Serial Number:
d6:2a:25:e3:89:22:4d:1b
Signature Algorithm: sha256WithRSAEncryption
Issuer: CN=localhost //used to locate issuers certificate
Validity
Not Before: Jun 13 14:34:28 2023 GMT
Not After : Jul 13 14:34:28 2023 GMT
Subject: CN=client //aka "user name"
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
RSA Public-Key: (2048 bit)
Modulus:
00:9c:7c:b4:e5:e9:3d:c1:70:9c:9d:18:2f:e8:a0:
The server verifies the certificate signature against trusted authorities, similar to validating a JWT's signature, then asks the client to send a "Certificate verify" message encrypted with the private key to prove possession of the key.
The Validation Chain
Certificate validation follows the PKIX process defined in RFC 5280. The server builds a certification path from the presented certificate to a trust anchor — a self-signed root certificate the validator inherently trusts. Certificate chains typically include intermediate CAs between the end-entity certificate and the root:
For each certificate in the chain, the validator checks the signature, validity period, allowed algorithms and key lengths, key usage, and required extensions. This complexity means every language and library implements validation slightly differently, creating fertile ground for bugs.
A Minimal mTLS Setup in Java
In a Java web application, enabling mTLS requires just a few configuration lines pointing to trusted root certificates:
$ cat application.properties
…
server.ssl.client-auth=need
server.ssl.trust-store=/etc/spring/server-truststore.p12
server.ssl.trust-store-password=changeit
On the client side, tools like curl must specify which certificate to present. The application code itself doesn't change from a standard web application:
$ curl -k -v –cert client.pem http://localhost/hello
This basic setup works well when a single root certificate signs all client certificates. It offers two main benefits:
- Speed: authentication occurs only during TLS handshake, and subsequent keep-alive requests are considered authenticated without CPU overhead.
- Storage: the server needs only the root certificate, not individual client certificates — similar to stateless JWT auth.
But it carries significant trade-offs:
- No granular control: if mTLS is on, all requests require authentication, even static assets.
- Any certificate signed by a trusted CA can authenticate, even those issued for other purposes.
- There is no host verification by default, so certificates can be accepted from any IP.
- Certificate issuance and rotation must be handled separately, adding operational burden.
Known Weaknesses in Certificate Handling
Several categories of attacks on certificate systems are well-documented:
- Signature forgery that alters certificate content while preserving the original signature completely breaks authentication.
- X.509 parsing can lead to buffer and heap overflows due to the format's complexity.
- Missing basic constraints checking allows end-entity certificates to sign additional certificates.
In Java specifically, most of these attacks are mitigated at the JDK API level: weak algorithms are rejected, and the PKIX implementation runs in memory-safe Java rather than native libraries. Rather than trying to exploit parsing vulnerabilities, this research focused on how application developers misuse the mTLS APIs.
Vulnerability 1: Certificate Extraction Mistakes
Applications almost always need access to the certificate presented during the TLS handshake, typically for authorization. Java provides two common approaches for accessing it:
X509Certificate[] certificates = sslSession.getPeerCertificates();
// another way
X509Certificate[] certificates = request.getAttribute("javax.servlet.request.X509Certificate");
A critical detail: this API returns an array of certificates because the TLS specification allows clients to send a full chain from end-entity to root CA. The mTLS RFC states that the sender's certificate MUST be first in this list, and the underlying TLS library only verifies the first certificate. Most applications correctly use the first element:
//way 1 is good
String user = certificates[0].getSubjectX500Principal().getName();
But some applications iterate through the array searching for a certificate matching specific criteria:
//way 2 is dangerous
for (X509Certificate cert : certificates) {
if (isClientCertificate(cert)) {
user = cert.getSubjectX500Principal().getName();
}
}
This is dangerous because the TLS library verifies only the first certificate and doesn't enforce chain ordering. A client can send a list where the first certificate is valid, and subsequent certificates are attacker-controlled — including self-signed certificates.
CVE-2023-2422: Keycloak Certificate Mix-Up
Keycloak, a widely-used authorization server supporting OAuth, SAML, and mTLS, fell into this trap. The implementation iterated over all presented certificates to find one matching the client_id form parameter. When a match was found, Keycloak trusted it implicitly, assuming signature verification had already happened during the handshake:
X509Certificate[] certs = null;
ClientModel client = null;
try {
certs = provider.getCertificateChain(context.getHttpRequest());
String client_id = null;
...
if (formData != null) {
client_id = formData.getFirst(OAuth2Constants.CLIENT_ID);
}
…
matchedCertificate = Arrays.stream(certs)
.map(certificate -> certificate.getSubjectDN().getName())
.filter(subjectdn -> subjectDNPattern.matcher(subjectdn).matches())
.findFirst();
The vulnerability is straightforward to exploit. An attacker sends a certificate chain where the first certificate is legitimate and chained to a trusted root CA, but a later certificate in the array is self-signed for a different user. The attacker doesn't even need a valid private key for that second certificate:
Keycloak offers several mTLS-enabled endpoints, but exploitation requires one without additional factors like tokens or secrets. The client-management/register-node endpoint fits this description because it mutates user data:
$ cat client1.crt client1.key > chain1.pem
$ curl --tlsv1.2 --tls-max 1.2 --cert chain1.pem -v -i -s -k "https://127.0.0.1:8443/realms/master/clients-managements/register-node?client_id=client1" -d "client_cluster_host=http://127.0.0.1:1213/"
A demonstration using openssl generates a self-signed certificate to append to the end of the certificate array:
$ openssl req -newkey rsa:2048 -nodes -x509 -subj /CN=client2 -out client2-fake.crt
$ cat client1.crt client1.key client2-fake.crt client1.key > chain2.pem
$ curl --tlsv1.2 --tls-max 1.2 --cert chain2.pem -v -i -s -k "https://127.0.0.1:8443/realms/master/clients-managements/register-node?client_id=client2" -d "client_cluster_host=http://127.0.0.1:1213/"
When the request is sent, Keycloak performs the action on behalf of the user in the self-signed certificate rather than the legitimate one, allowing unauthorized data mutations for any client using mTLS. The fix was simple: use only the first certificate in the array. This CVE illustrates how well-intentioned APIs and interfaces can be misused.
Risk: Certificate Forwarding via Proxy
Many mTLS deployments terminate TLS at a reverse proxy, which then forwards the certificate to backend servers as an HTTP header. A typical nginx configuration for this pattern:
$ cat nginx.conf
http {
server {
server_name example.com;
listen 443 ssl;
…
ssl_client_certificate /etc/nginx/ca.pem;
ssl_verify_client on;
location / {
proxy_pass http://host.internal:80;
proxy_set_header ssl-client-cert $ssl_client_cert;
}
}
In these setups, backend servers usually skip additional validation, implicitly trusting the proxy. While not directly exploitable, this pattern carries substantial risk:
- Any server on the local network can send this header, so the proxy-to-backend segment must be isolated from external traffic.
- If the proxy or backend suffers from request smuggling or header injection — and recent CVEs in Netty and Node.js show these aren't rare — those attacks become trivial to convert into full impersonation.
The safer approach is to have every server that relies on certificate authentication independently verify the certificate's signature whenever possible, rather than blindly trusting whatever arrives in a header.
Chain Validation: Following Data Before Verifying Signatures
In larger systems, servers often do not maintain root and intermediate certificates locally. Instead, they may rely on external storage for certificate chain validation. RFC 4387 describes a certificate store interface that enables lazy access to certificates during validation—commonly implemented over protocols such as HTTP, LDAP, FTP, or SQL.

Certain X.509 extensions defined by RFC 3280 carry information about locating issuer or CA certificates. The Authority Information Access (AIA) extension, for example, may contain a URL pointing to the issuer’s certificate. If an application leverages this extension during validation, it becomes a prime target for SSRF attacks. Similarly, fields like Subject, Issuer, and Serial can be leveraged to build LDAP or SQL queries, introducing injection vectors.

When certificate stores are involved, treat these values as untrusted input—insertion points akin to what security testers mark in Burp Suite’s Intruder. Notably, these values can be used in queries before the certificate’s signature is ever checked, making them especially attractive to attackers.
Case Study: CVE-2023-33201 in Bouncy Castle
A concrete example is LDAPCertStore from Bouncy Castle, a widely used Java library for certificate handling. The following snippet shows how a certificate chain can be constructed and validated using this store.
PKIXBuilderParameters pkixParams = new PKIXBuilderParameters(keystore, selector);
//setup additional LDAP store
X509LDAPCertStoreParameters CertStoreParameters = new X509LDAPCertStoreParameters.Builder("ldap://127.0.0.1:1389", "CN=certificates").build();
CertStore certStore = CertStore.getInstance("LDAP", CertStoreParameters, "BC");
pkixParams.addCertStore(certStore);
// Build and verify the certification chain
try {
CertPathBuilder builder = CertPathBuilder.getInstance("PKIX", "BC");
PKIXCertPathBuilderResult result =
(PKIXCertPathBuilderResult) builder.build(pkixParams);
Internally, Bouncy Castle uses the certificate’s Subject field to assemble an LDAP query without escaping the input—an obvious flaw.

Special characters in the Subject can alter the LDAP query syntax, typically resulting in blind LDAP injection. Depending on the application’s error handling and directory structure, this could allow an attacker to extract additional data from the LDAP directory. The fix, applied in Bouncy Castle as a patch for this CVE, was to properly filter special characters when constructing the query.
Revocation: A Hidden Attack Surface
Certificate chains gain trust through signatures alone, much like JSON Web Tokens. Revocation, however, introduces complications. The PKIX specification (RFC 4387) outlines special stores for revoked certificates, accessible over HTTP or LDAP. Developers remain divided on revocation: some consider it essential, while others avoid it due to performance overhead or rely solely on offline revocation lists.

The location of the revocation store can either be hardcoded in the application or derived from the certificate itself via the AIA OCSP URL or the CRL Distribution Points (CRLDP) extension. From a security standpoint, it is remarkable that an application would fetch a revocation server URL from an untrusted certificate. If the application uses AIA or CRLDP URLs for revocation checks, it can be abused for SSRF attacks. Such requests typically occur after signature verification, but they remain exploitable in certain configurations.
In Java, LDAP is also a supported protocol for revocation lookups. Historically, unmarshaling LDAP responses has led to remote code execution—a finding reported by Moritz Bechler, later patched in the JDK (see his analysis). Bouncy Castle, by comparison, can be configured to use the CRLDP extension to contact LDAP servers. However, it only fetches a specific attribute and does not process references, which rules out RCE; HTTP-based SSRF remains a viable threat vector.
private static Collection getCrlsFromLDAP(CertificateFactory certFact, URI distributionPoint) throws IOException, CRLException
{
Map<String, String> env = new Hashtable<String, String>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, distributionPoint.toString());
byte[] val = null;
try
{
DirContext ctx = new InitialDirContext((Hashtable)env);
Attributes avals = ctx.getAttributes("");
Attribute aval = avals.get("certificateRevocationList;binary");
val = (byte[])aval.get();
}
Case Study: CVE-2023-28857 in Apereo CAS
Apereo CAS, a highly configurable authentication server, offers administrators the option to enable certificate revocation checks against an external LDAP server:
cas.authn.x509.crl-fetcher=ldap
cas.authn.x509.ldap.ldap-url=ldap://example.com:1389/
cas.authn.x509.ldap.bind-dn=admin
cas.authn.x509.ldap.bind-credential=s3cr3taaaaa
When this setting is active, Apereo CAS retrieves the CRLDP URL from the client’s certificate to perform the revocation lookup.
/**
* Validate the X509Certificate received.
*
* @param cert the cert
* @throws GeneralSecurityException the general security exception
*/
private void validate(final X509Certificate cert) throws GeneralSecurityException {
cert.checkValidity();
this.revocationChecker.check(cert);
val pathLength = cert.getBasicConstraints();
if (pathLength < 0) {
if (!isCertificateAllowed(cert)) {
val msg = "Certificate subject does not match pattern " + this.regExSubjectDnPattern.pattern();
LOGGER.error(msg);
Initial concerns about RCE proved unfounded—Apereo CAS relies on a custom LDAP library that doesn’t support the object factories or codebases necessary for exploitation. Testing revealed a different flaw, however: the server prioritizes the LDAP URL embedded within the certificate over the one administrators configure in settings. Despite this, it still sends the configured password alongside the lookup. A simple test using a self-signed certificate with a CRLDP extension pointing to a netcat listener confirmed the issue:

The credentials were transmitted to the attacker’s listener. Apereo CAS developers responded swiftly, patching the vulnerability within a day by clearing LDAP credentials whenever the URL is sourced from the CRLDP extension. While the leak is fixed, relying on CRLDP-supplied URLs still remains risky and broadens the attack surface unnecessarily.
Key Takeaways
For those building mTLS systems or conducting security assessments, keep these points in mind:
- When extracting usernames or other identifiers from the mTLS chain, remember that servers only validate the first certificate in the presented chain.
- Certificate stores introduce query injection risks (LDAP, SQL). Treat all certificate fields used in lookups as untrusted input.
- Revocation checks can lead to SSRF, or in severe cases, remote code execution. Always perform revocation checks after other validations and avoid using URLs taken from certificate extensions.



