2

Edit: BNK in comments has linked to a solution found here.

I'm sending off POST requests via REST to a backend server (over LAN), all done over HTTPS. This server has a self signed certificate as a .pem file, everything works okay.

I'm now trying to connect to a different web server (over WAN, through DNS), a self signed certificate also but as a .crt file (standard, BER/DER format). However now, although the code is the same, I am receiving the following exception:

java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.

I'm not sure why one server is okay to connect but the other is not. I do not want to trust all certificates as this will be going over the public internet.

My network code:

public HttpsURLConnection setUpHttpsConnection(String urlString)
{
    try
    {
        // Load CAs from an InputStream
        CertificateFactory cf = CertificateFactory.getInstance("X.509");

        InputStream caInput = new BufferedInputStream(context.getAssets().open("server.crt"));
        Certificate ca = cf.generateCertificate(caInput);
        System.out.println("ca=" + ((java.security.cert.X509Certificate) ca).getSubjectDN());

        // Create a KeyStore containing our trusted CAs
        String keyStoreType = KeyStore.getDefaultType();
        KeyStore keyStore = KeyStore.getInstance(keyStoreType);
        keyStore.load(null, null);
        keyStore.setCertificateEntry("ca", ca);

        // Create a TrustManager that trusts the CAs in our KeyStore
        String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
        TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
        tmf.init(keyStore);

        // Create an SSLContext that uses our TrustManager
        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(null, tmf.getTrustManagers(), null);

        // Create all-trusting host name verifier
        //  to avoid the following :
        //   java.security.cert.CertificateException: No name matching
        // This is because Java by default verifies that the certificate CN (Common Name) is
        // the same as host name in the URL. If they are not, the web service client fails.
        HostnameVerifier allHostsValid = new HostnameVerifier() {
            @Override
            public boolean verify(String arg0, SSLSession arg1) {
                return true;
            }
        };
        // Install it
        HttpsURLConnection.setDefaultHostnameVerifier(allHostsValid);

        // Tell the URLConnection to use a SocketFactory from our SSLContext
        URL url = new URL(urlString);
        HttpsURLConnection urlConnection = null;
        urlConnection = (HttpsURLConnection)url.openConnection();
        urlConnection.setSSLSocketFactory(sslContext.getSocketFactory());

        return urlConnection;
    }
    catch (Exception ex)
    {
        Log.e("NetworkManager", "Failed to establish SSL connection to server: " + ex.toString());
        return null;
    }
}

/**
 * Represents an asynchronous login/registration task used to authenticate
 * the user.
 */
public class POSTTask extends AsyncTask<POSTRequest, Void, StringBuilder>
{
    POSTTask()
    {
    }

    @Override
    protected void onPreExecute() {}

    @Override
    protected StringBuilder doInBackground(POSTRequest... params)
    {
        OutputStream os = null;

        try {
            HttpsURLConnection urlConnection = setUpHttpsConnection(params[0].url);
            //Sets the maximum time to wait for an input stream read to complete before giving up.
            urlConnection.setReadTimeout(30000);
            //Sets the maximum time in milliseconds to wait while connecting.
            urlConnection.setConnectTimeout(20000);
            urlConnection.setRequestMethod("POST");
            urlConnection.setDoInput(true);
            urlConnection.setDoOutput(true);

            UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(params[0].nameValuePairs);
            os = urlConnection.getOutputStream();
            formEntity.writeTo(os);

            InputStream in = urlConnection.getInputStream();
            StringBuilder ret = inputStreamToString(in);

            return ret;

        } catch (IOException e) {
            Log.i("NetworkError", e.toString());
        } catch (Exception e) {

        } finally {
            if (os != null) {
                try {
                    os.close();
                } catch (IOException ex) {
                }
            }
        }             
        return null;
    }

    @Override
    protected void onPostExecute(StringBuilder result) {
    }

    @Override
    protected void onCancelled() {
    }
}
Community
  • 1
  • 1
LKB
  • 1,020
  • 5
  • 22
  • 46
  • Did the server get that exception, or the client? – user207421 Aug 31 '15 at 04:30
  • @EJP, sorry, the client (my Android app). – LKB Aug 31 '15 at 04:30
  • 2
    Not related to the question but to your code: disabling the hostname check like you do effectively an attacker to use its **any** certificate signed by a trusted CA for a man-in-the-middle attack. Since such a certificate is trivially to get (hacker needs to own a domain name) you effectively disable any kind of certificate checks this way. – Steffen Ullrich Aug 31 '15 at 04:35
  • @SteffenUllrich, thank you for the advice! How can I combat that? If I remove those lines of code and try to connect to the server that works fine for me I get "hostname not verified". – LKB Aug 31 '15 at 04:40
  • 2
    @LBran: the proper way is of course to have the certificate match the name. And the best way to deal with self-signed certificates is to not use the systems trust store at all but only trust specific certificates. For more details and example code head over to [OWASP: certificate and public key pinning](https://www.owasp.org/index.php/Certificate_and_Public_Key_Pinning). – Steffen Ullrich Aug 31 '15 at 04:43
  • @SteffenUllrich, that makes sense, thanks! Yes I'd ideally like to only trust one specific self-signed certificate... – LKB Aug 31 '15 at 04:46
  • If the client got that exception, it doesn't trust the server certificate. May that certificate is self-signed too? – user207421 Aug 31 '15 at 05:05
  • @EJP thank you for your response. Yes, the other certificate is self-signed. – LKB Aug 31 '15 at 05:16
  • 1
    Well if it's your server get it signed by a CA. It's cheaper in the long run. If it isn't your server, complain. In the short term you can import their cert to your trust store but it's not the way to go for production. – user207421 Aug 31 '15 at 06:38
  • Thanks @EJP, I've noted this down. I'm still experiencing the issue RE trust anchor for certification path not found. Do you think it's likely to be a config issue? Seeing as I'm adding the certificate to the trust store (in above code) so it *should* be trusting it... – LKB Aug 31 '15 at 06:56
  • You're adding the *second* server's certificate via that code? – user207421 Aug 31 '15 at 06:58
  • @EJP Yeah I have two certificates in my assets folder - one for each server. I've just changed the certificate name so assets pulls up the right certificate. I only need one certificate at a time. The line `InputStream caInput = new BufferedInputStream(context.getAssets().open("server.pem"));` – LKB Aug 31 '15 at 07:00
  • 1
    Hi! IMO, you can try my answer at [this question](http://stackoverflow.com/questions/32154115/android-volley-self-signed-https-trust-anchor-for-certification-path-not-found/32219177#32219177) to check if it works for your case or not. Pay attention to 'getWrappedTrustManagers'. – BNK Aug 31 '15 at 10:20
  • 1
    @BNK Hey! Thanks for your response - I can confirm that this works and is fine for a temporary fix. This is just for a demo so will suffice, but I definitely need to fix a few things so I'm not trusting *everything*. :) – LKB Aug 31 '15 at 11:00
  • @LBran everything? Do you mean HostnameVerifier? – BNK Aug 31 '15 at 11:11
  • @BNK Does the answer you linked to not bypass checks that are integral to the secure transmission of data? – LKB Aug 31 '15 at 11:20
  • Sorry for my bad English? I don't understand much. – BNK Aug 31 '15 at 11:25
  • @SteffenUllrich: would you please tell me if the code in my answer can prevent an attacker to use any certificate signed by a trusted CA for a man-in-the-middle attack or not? And how to emulate that case? Thanks in advance! – BNK Aug 31 '15 at 14:15
  • @BNK: If I understand your code correctly you simple expect a hostname in the certificate which is different from the name given in the URL. As long as the name you expect is not one an attacker can get a certificate for (which is unlikely) this should be a secure work around in case you need to trust a certificate which contains the wrong name. – Steffen Ullrich Aug 31 '15 at 15:07
  • @SteffenUllrich: you mean that if I replace "localhost" by the name given in the URL, that would be unsafe? – BNK Aug 31 '15 at 15:12
  • @BNK: since a problem seems to be that the hostname from the URL does not match the name inside the certificate it would not be unsafe, but it would simply not work because then the expected hostname (from the URL) would not be found in the certificate. – Steffen Ullrich Aug 31 '15 at 15:13
  • @SteffenUllrich: as long as the hostname (verify's parameter) is the same as "Issued to" in the server cerificate, it works, this hostname can be the same as or different from the hostname (in the URL) – BNK Aug 31 '15 at 15:20
  • @BNK: correct, but if the hostname in the URL is already the hostname in the certificate then you would not need to write a custom HostnameVerifier at all. – Steffen Ullrich Aug 31 '15 at 15:21
  • @SteffenUllrich: do you mean that I don't need to implement `HttpsURLConnection.setDefaultHostnameVerifier(...)`? I have not tested it yet :) – BNK Aug 31 '15 at 15:24
  • 1
    @BNK: from the [documentation](http://docs.oracle.com/javase/7/docs/api/javax/net/ssl/HostnameVerifier.html) I read that the hostname verifier is only used " if the URL's hostname and the server's identification hostname mismatch". So no need to use one of the hostname from the URL is contained in the certificate. – Steffen Ullrich Aug 31 '15 at 15:36
  • @SteffenUllrich many thanks, [Google's document](http://developer.android.com/reference/javax/net/ssl/HostnameVerifier.html) also says that. I haven't read carefully :-) – BNK Sep 01 '15 at 00:16

1 Answers1

3

If I correctly understand your idea about "all trusting", which is hostname verifier in your code, you can refer to the following:

Let's assume your server app is hosting inside IIS which has a server certificate in which "Issued to" is "localhost", for example. Then, inside verify method you can verify "localhost".

HostnameVerifier hostnameVerifier = new HostnameVerifier() {
    @Override
    public boolean verify(String hostname, SSLSession session) {
        HostnameVerifier hv =
            HttpsURLConnection.getDefaultHostnameVerifier();
        return hv.verify("localhost", session);
    }
};
BNK
  • 23,994
  • 8
  • 77
  • 87