24

I need to ignore the PKIX path building exception

javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException:
PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderExc
ption: unable to find valid certification path to requested target

I know how to do this by writing my own class implementing X509TrustManager where I always return true from isServerTrusted.

However, I don't want to trust all servers & all clients.

  • I want all the default verification to be done for clients as is done currently.
  • For servers, I want to ignore server cert verification only for one particular cert but want to go ahead and verify it as is done currently (for eg. using cacerts store).

How can I achieve something like this - i.e. pass on part of the verification to whatever was the X509TrustFactory object before I replaced it.

i.e. this is what I want to do

public boolean isServerTrusted(X509Certificate[] chain)
{
    if(chain[0].getIssuerDN().getName().equals("MyTrustedServer") && chain[0].getSubjectDN().getName().equals("MyTrustedServer"))
        return true;

    // else I want to do whatever verification is normally done
}

Also I don't want to disturb the existing isClientTrusted verification.

How can I do this?

user93353
  • 13,733
  • 8
  • 60
  • 122

3 Answers3

55

You can get hold of the existing default trust manager and wrap it in your own using something like this:

TrustManagerFactory tmf = TrustManagerFactory
        .getInstance(TrustManagerFactory.getDefaultAlgorithm());
// Using null here initialises the TMF with the default trust store.
tmf.init((KeyStore) null);

// Get hold of the default trust manager
X509TrustManager x509Tm = null;
for (TrustManager tm : tmf.getTrustManagers()) {
    if (tm instanceof X509TrustManager) {
        x509Tm = (X509TrustManager) tm;
        break;
    }
}

// Wrap it in your own class.
final X509TrustManager finalTm = x509Tm;
X509TrustManager customTm = new X509TrustManager() {
    @Override
    public X509Certificate[] getAcceptedIssuers() {
        return finalTm.getAcceptedIssuers();
    }

    @Override
    public void checkServerTrusted(X509Certificate[] chain,
            String authType) throws CertificateException {
        finalTm.checkServerTrusted(chain, authType);
    }

    @Override
    public void checkClientTrusted(X509Certificate[] chain,
            String authType) throws CertificateException {
        finalTm.checkClientTrusted(chain, authType);
    }
};

SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[] { customTm }, null);

// You don't have to set this as the default context,
// it depends on the library you're using.
SSLContext.setDefault(sslContext);

You can then implement your own logic around finalTm.checkServerTrusted(chain, authType);.

However, you should make sure you're making an exception for the specific certificate you want to ignore.

What you're doing in the following is letting through any certificate with these Issuer DN and Subject DN (which isn't difficult to forge):

if(chain[0].getIssuerDN().getName().equals("MyTrustedServer") && chain[0].getSubjectDN().getName().equals("MyTrustedServer"))
    return true;

You could instead load the X509Certificate instance from a known reference and compare the actual value in the chain.

In addition, checkClientTrusted and checkServerTrusted are not methods that return true or false, but void methods that will succeed silently by default. If there's something wrong with the certificate you expect, throw a CertificateException explicitly.

Bruno
  • 119,590
  • 31
  • 270
  • 376
  • 15
    +1 many thanks for the comment _"// Using null here initialises the TMF with the default trust store."_. It would be nice if this was in the api docs ;) – Dori Mar 04 '14 at 13:26
  • Can you please explain and show me an example of this part: **You could instead load the X509Certificate instance from a known reference and compare the actual value in the chain.** – OnePunchMan Jun 10 '14 at 07:41
  • @kaze, if you want to compare *exact* certificates, you can load an X.509 certificate from any source you want (e.g. keystore of PEM file via a CertificateFactory), then you can compare what's presented in the chain (element 0) with the reference instance. Not sure what `equals` compare, but you can certainly compare the results from `getEncoded()` on both `X509Certificate` instances (byte array comparison, of course, not references). – Bruno Jul 03 '14 at 19:09
  • @Dori Seriously. This post deserves an up-vote for that alone. – Brent Writes Code Nov 06 '14 at 19:50
  • @OnePunchMan you can store the `PEM`-encoded certificate in your classpath and compare the `X509Certificate` instance itself. As soon as an attacker is unable to tamper your jar with a fake certificate, you have full control of your trust store – usr-local-ΕΨΗΕΛΩΝ Mar 01 '17 at 08:50
  • Hello @bruno I have a question, why are we creating finalTm, can't be have write the cert checking logic inside the implementation of customTm itself? Thanks! – java_doctor_101 Apr 04 '17 at 19:56
  • @nitinsh99 You can, but re-implementing that logic can actually be quite complex (see [documentation](http://docs.oracle.com/javase/8/docs/technotes/guides/security/certpath/CertPathProgGuide.html), and you need to check the right attributes for TLS and so on...). This is why it makes sense to pass part of the verification to the existing verifier. – Bruno Apr 05 '17 at 07:29
  • What if someone wants to create a custom TrustManager that just overrides checkServerTrusted() method's behavior (say to perform some revocation checks on the received server certificates), but keeps the getAccepterIssuers() behavior the same? Will it work if we return `null` or `new X509Certificate[0];` from `getAcceptedIssuers()`? What exactly happens if we return `null` or `new X509Certificate[0];` from there? – MediumOne Nov 27 '17 at 06:24
  • @MediumOne `getAcceptedIssuers` is only really useful in conjunction with `checkClientTrusted`, since it's what gives a hint to client regarding which client certificate they should send. In addition, it should never return `null` in principle, but a 0-length array instead, although it seems to work with both. (See [this answer](https://stackoverflow.com/a/15223905/372643) for behaviour.) – Bruno Nov 27 '17 at 16:42
  • @Bruno I just faced such issue with Self-Signed cert., and ended up grabbing `getAcceptedIssuers()` from wrapped finalTm, and compare them to the first cert in chain. – Chakrit W Mar 05 '21 at 02:36
8

Instead of implementing X509TrustManager to trust any certificate, you can create a trust manager from the specific certificate in question. Load the certificate from a .p12 or .jks keystore or from a .crt-file (you can copy a certificate from the browser into a file, in Chrome by clicking the padlock and selecting Certificate). The code is shorter than implementing your own X509TrustManager:

private static SSLSocketFactory createSSLSocketFactory(File crtFile) throws GeneralSecurityException, IOException {
    SSLContext sslContext = SSLContext.getInstance("SSL");

    // Create a new trust store, use getDefaultType for .jks files or "pkcs12" for .p12 files
    KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
    // You can supply a FileInputStream to a .jks or .p12 file and the keystore password as an alternative to loading the crt file
    trustStore.load(null, null);

    // Read the certificate from disk
    X509Certificate result;
    try (InputStream input = new FileInputStream(crtFile)) {
        result = (X509Certificate) CertificateFactory.getInstance("X509").generateCertificate(input);
    }
    // Add it to the trust store
    trustStore.setCertificateEntry(crtFile.getName(), result);

    // Convert the trust store to trust managers
    TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
    tmf.init(trustStore);
    TrustManager[] trustManagers = tmf.getTrustManagers();

    sslContext.init(null, trustManagers, null);
    return sslContext.getSocketFactory();
}

You can use it by calling HttpsURLConnection.setSSLSocketFactory(createSSLSocketFactory(crtFile)) (you probably want to initialize the socket factory once and reuse it, though).

Johannes Brodwall
  • 7,673
  • 5
  • 34
  • 28
0

Although this question has a valid answer, I want to propose an alternative which requires less custom code. I would also like to mentioned I created this library to simplify the ssl setup.

It seems like with this kind of option you want trust the counter party (client or server) when a custom condition is true or else fall back to the default validation of the trustmanager. You could try the following snippet:

SSLFactory sslFactory = SSLFactory.builder()
          .withDefaultTrustMaterial() // uses jdk trusted certificates
          .withTrustEnhancer(((X509Certificate[] certificateChain, String authType, SSLEngine sslEngine) -> "Foo".equals(certificateChain[0].getIssuerX500Principal().getName()))
          .build();

SSLContext sslContext = sslFactory.getSslContext();

It will create a custom trustmanager for you and use that within the SSLContext, so you don't need to specify a custom one. See here for more: GitHub - SSLContext Kickstart I am maintaining this library and hopefully it will be useful for others.

Hakan54
  • 3,121
  • 1
  • 23
  • 37