SPKI pinning TLS strategy
HttpClient 5.6 adds an opt-in SPKI pinning TLS strategy for clients that need an additional defence layer on top of the normal PKI and hostname verification.
The implementation lives in:
org.apache.hc.client5.http.ssl.SpkiPinningClientTlsStrategy
and is used as a drop-in replacement for the regular client-side TLS strategy in both classic and async connection managers.
:warning: Warning: Certificate pinning increases operational risk. Misconfigured pins can brick your client fleet until you ship an update. Always keep normal PKI and hostname verification enabled, and ship at least one backup pin for each host.
What is pinned?
Pins are applied to the SubjectPublicKeyInfo (SPKI) of certificates in the peer chain, not to raw certificate bytes.
Each pin has the form:
sha256/<base64(SPKI)>
For example:
sha256/qrvdF0L7Kp5l3H8k0m3x7VZq3p5O6s4L4kC2Z7tZt+Q=
Only sha256/… pins are accepted. Any other prefix, invalid Base64, or wrong
decoded length (not 32 bytes) will cause an IllegalArgumentException during
builder configuration.
Host patterns and wildcards
Pins are configured per host pattern, which can be:
- an exact host (for example
api.example.com), or - a single-label wildcard (for example
*.example.com).
Behaviour:
- Host names are normalised to IDNA ASCII (Punycode) and lowercased.
*.example.commatchesa.example.com,b.example.com, etc.*.example.comdoes not matcha.b.example.com.- Wildcards must be single-label; patterns like
"*."are rejected.
How enforcement works
SpkiPinningClientTlsStrategy decorates the default client TLS strategy:
- The normal trust manager and hostname verification run first.
- If neither of those fails, pinning logic is applied:
- It finds all rules whose host pattern matches the target host.
- It computes
SHA-256(SPKI)for allX509Certificateentries in the peer chain. - If at least one configured pin matches at least one SPKI hash, pinning passes.
- Otherwise an
SSLExceptionis thrown and the connection fails.
If no rules match the host, pinning is skipped and only normal PKI rules apply.
Classic client configuration
To enable SPKI pinning for the classic (CloseableHttpClient) API, plug the
strategy into a PoolingHttpClientConnectionManager:
import javax.net.ssl.SSLContext;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
import org.apache.hc.client5.http.ssl.SpkiPinningClientTlsStrategy;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.message.StatusLine;
import org.apache.hc.core5.ssl.SSLContexts;
public final class ClientSpkiPinningExample {
public static void main(final String[] args) throws Exception {
final SSLContext sslContext = SSLContexts.createSystemDefault();
final SpkiPinningClientTlsStrategy pinning = SpkiPinningClientTlsStrategy
.newBuilder(sslContext)
// Replace these with real host(s) and real pins (sha256/<base64(SPKI)>)
.add("example.com",
"sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
"sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=") // backup
.add("*.example.net",
"sha256/CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC=")
.build();
final PoolingHttpClientConnectionManager cm =
PoolingHttpClientConnectionManagerBuilder.create()
.setTlsSocketStrategy(pinning)
.build();
try (final CloseableHttpClient httpclient = HttpClients.custom()
.setConnectionManager(cm)
.build()) {
final HttpGet httpget = new HttpGet("https://example.com/");
System.out.println("Executing: " + httpget.getMethod() + " " + httpget.getUri());
httpclient.execute(httpget, response -> {
System.out.println("----------------------------------------");
System.out.println(httpget + " -> " + new StatusLine(response));
EntityUtils.consume(response.getEntity());
return null;
});
}
}
}
If the peer certificate chain does not contain any of the configured pins for
example.com, the TLS handshake will fail with an SSLException explaining
which pins were configured and which SPKI hashes were observed.
Async client configuration
For the async client (CloseableHttpAsyncClient) the approach is analogous,
but you plug the strategy into PoolingAsyncClientConnectionManagerBuilder:
import javax.net.ssl.SSLContext;
import org.apache.hc.client5.http.async.methods.SimpleHttpRequest;
import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder;
import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
import org.apache.hc.client5.http.impl.async.HttpAsyncClients;
import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManager;
import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder;
import org.apache.hc.client5.http.ssl.SpkiPinningClientTlsStrategy;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.nio.support.BasicRequestProducer;
import org.apache.hc.core5.http.nio.support.BasicResponseConsumer;
import org.apache.hc.core5.http.nio.entity.StringAsyncEntityConsumer;
import org.apache.hc.core5.reactor.IOReactorConfig;
import org.apache.hc.core5.util.Timeout;
import org.apache.hc.core5.ssl.SSLContexts;
public final class AsyncSpkiPinningExample {
public static void main(final String[] args) throws Exception {
final SSLContext sslContext = SSLContexts.createSystemDefault();
final SpkiPinningClientTlsStrategy pinning = SpkiPinningClientTlsStrategy
.newBuilder(sslContext)
.add("api.example.com",
"sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
"sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=")
.build();
final PoolingAsyncClientConnectionManager cm =
PoolingAsyncClientConnectionManagerBuilder.create()
.setTlsStrategy(pinning)
.setIoReactorConfig(IOReactorConfig.custom()
.setSoTimeout(Timeout.ofSeconds(5))
.build())
.build();
try (final CloseableHttpAsyncClient client = HttpAsyncClients.custom()
.setConnectionManager(cm)
.build()) {
client.start();
final SimpleHttpRequest request = SimpleRequestBuilder
.get("https://api.example.com/data")
.build();
final SimpleHttpResponse response = client.execute(
HttpHost.create("https://api.example.com"),
new BasicRequestProducer(request, null),
new BasicResponseConsumer<>(new StringAsyncEntityConsumer()),
null).get();
System.out.println(response.getCode() + " " + response.getReasonPhrase());
}
}
}
(Adjust host, pins and request details to your environment.)
Failure modes
- Pinning failure: if no configured pin matches any SPKI in the peer
chain,
SpkiPinningClientTlsStrategythrows anSSLExceptionwith a message containing:- the computed peer pins, and
- the configured pins for the matching rules.
- Configuration errors (invalid Base64, wrong length, invalid wildcard,
empty pin list, etc.) are signalled as
IllegalArgumentExceptionat build time. - No matching rules: pinning is skipped and only the normal trust / hostname checks apply.
Testing and reference implementation
The behaviour of SpkiPinningClientTlsStrategy is covered by:
These tests validate:
- exact host and wildcard matches,
- IDN normalisation (
bücher.example), - backup pins,
- invalid or missing pins,
- cases where no rules apply and pinning is bypassed.
Summary
- Opt-in, immutable TLS strategy for SPKI pinning:
SpkiPinningClientTlsStrategy. - Pins are
sha256/<base64(SPKI)>overSubjectPublicKeyInfo. - Supports exact hosts and single-label wildcards with IDN handling.
- Applied after normal PKI and hostname verification.
- Integrates with both classic and async connection managers.




