square/okhttp · critical · IOException

Denylisted peer certificate:

Error message

Denylisted peer certificate: 

What it means

A custom OkHttp network interceptor (CHECK_HANDSHAKE_INTERCEPTOR) computes CertificatePinner.pin(certificate) for each peer certificate in the established TLS handshake and compares it against a hard-coded denylist (sha256/afwi3RxoMmLkuRW1l7QsPZTJPwDS2jXw8ig=). On a match it throws java.io.IOException("Denylisted peer certificate: " + pin), aborting the call inside the interceptor chain. This is a deliberate security policy (e.g. revoking a specific compromised cert) implemented in user code, not an OkHttp built-in.

Source

Thrown at samples/guide/src/main/java/okhttp3/recipes/CheckHandshake.java:38

import java.util.Collections;
import java.util.Set;
import okhttp3.CertificatePinner;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

public final class CheckHandshake {
  /** Rejects otherwise-trusted certificates. */
  private static final Interceptor CHECK_HANDSHAKE_INTERCEPTOR = new Interceptor() {
    final Set<String> denylist = Collections.singleton(
        "sha256/afwiKY3RxoMmLkuRW1l7QsPZTJPwDS2pdDROQjXw8ig=");

    @Override public Response intercept(Chain chain) throws IOException {
      for (Certificate certificate : chain.connection().handshake().peerCertificates()) {
        String pin = CertificatePinner.pin(certificate);
        if (denylist.contains(pin)) {
          throw new IOException("Denylisted peer certificate: " + pin);
        }
      }
      return chain.proceed(chain.request());
    }
  };

  private final OkHttpClient client = new OkHttpClient.Builder()
      .addNetworkInterceptor(CHECK_HANDSHAKE_INTERCEPTOR)
      .build();

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("https://publicobject.com/helloworld.txt")
        .build();

    try (Response response = client.newCall(request).execute()) {
      if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

View on GitHub (pinned to 4fc0831380)

Solutions

  1. Confirm whether the denylist entry is still intended — review the sha256 constant against current revocation needs.
  2. If the cert rotation is legitimate, remove or update the denylisted pin.
  3. Distinguish this IOException by message prefix in your catch block so denylist rejections are reported as security events, not generic failures.
  4. Prefer OkHttp's CertificatePinner for allow-listing instead of a hand-rolled denylist where possible.

Example fix

// before
if (denylist.contains(pin)) {
  throw new IOException("Denylisted peer certificate: " + pin);
}

// after — typed exception + structured logging
if (denylist.contains(pin)) {
  throw new DenylistedCertificateException(pin, chain.connection().route());
}
// caller:
try {
  client.newCall(request).execute();
} catch (DenylistedCertificateException e) {
  securityLog.warn("Refused denylisted cert pin={} host={}", e.pin, e.route.address().url().host());
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Before making the call, confirm the expected peer cert is NOT denylisted
String expectedPin = "sha256/<expected-current-cert-spki>";
if (Set.of("sha256/afwiKY3RxoMmLkuRW1l7QsPZTJPwDS2pdDROQjXw8ig=").contains(expectedPin)) {
  throw new IllegalStateException("Configured cert matches a denylisted pin; aborting before request");
}

Try / catch

try {
  Response response = client.newCall(request).execute();
} catch (IOException e) {
  if (e.getMessage() != null && e.getMessage().startsWith("Denylisted peer certificate:")) {
    String pin = e.getMessage().substring(e.getMessage().indexOf(':') + 1).trim();
    securityLogger.warn("Denylisted cert presented pin={} host={}", pin, request.url().host());
    throw new SecurityPolicyException("Denylisted certificate", e);
  }
  throw e;
}

Prevention

When it happens

Trigger: Any client.newCall(...).execute() through this client when the server's leaf certificate SPKI hash equals the denylisted value. Concretely: the server presents the revoked cert, the TLS handshake completes (cert is otherwise trusted), the interceptor runs, computes the pin, matches the denylist, and throws before chain.proceed().

Common situations: Intentionally triggered during a security test that points the client at a server using the revoked cert; accidentally triggered when the upstream legitimately rotated to a cert whose SPKI happens to match the denylisted hash (collision is astronomically unlikely but the denylist is meant to be maintained); deploying this interceptor against a CDN whose cert chain changes per-PoP.

Related errors


AI-assisted analysis of square/okhttp@4fc0831380 (2026-08-04). Data as JSON: /data/errors/e2590c0f9858a099.json. Report an issue: GitHub.