1
votes

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() {
    }
}
1
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
@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.Steffen Ullrich
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
Hi! IMO, you can try my answer at this question to check if it works for your case or not. Pay attention to 'getWrappedTrustManagers'.BNK
@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

1 Answers

3
votes

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);
    }
};