In a previous post I described my frustration with the fact that it’s so difficult to find documentation about how to connect to a server using HTTPS if the certificate for that server is self-signed (not from a paid-for certificate authority).
After a while I found that someone at Google noticed that because of their lack of documentation the common solution is to disable certificate checking altogether, which of course nullifies any possible advantage of using HTTPS. So they posted some documentation, and after struggling with it for a while I finally got it to work.
You don’t need to use BouncyCastle, just the stock Android APIs, you should be able to easily customise this code for your own uses:
/** * Set up a connection to littlesvr.ca using HTTPS. An entire function * is needed to do this because littlesvr.ca has a self-signed certificate. * * The caller of the function would do something like: * HttpsURLConnection urlConnection = setUpHttpsConnection("https://littlesvr.ca"); * InputStream in = urlConnection.getInputStream(); * And read from that "in" as usual in Java * * Based on code from: * https://developer.android.com/training/articles/security-ssl.html#SelfSigned */ @SuppressLint("SdCardPath") public static HttpsURLConnection setUpHttpsConnection(String urlString) { try { // Load CAs from an InputStream // (could be from a resource or ByteArrayInputStream or ...) CertificateFactory cf = CertificateFactory.getInstance("X.509"); // My CRT file that I put in the assets folder // I got this file by following these steps: // * Go to https://littlesvr.ca using Firefox // * Click the padlock/More/Security/View Certificate/Details/Export // * Saved the file as littlesvr.crt (type X.509 Certificate (PEM)) // The MainActivity.context is declared as: // public static Context context; // And initialized in MainActivity.onCreate() as: // MainActivity.context = getApplicationContext(); InputStream caInput = new BufferedInputStream(MainActivity.context.getAssets().open("littlesvr.crt")); Certificate ca = cf.generateCertificate(caInput); System.out.println("ca=" + ((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 context = SSLContext.getInstance("TLS"); context.init(null, tmf.getTrustManagers(), null); // Tell the URLConnection to use a SocketFactory from our SSLContext URL url = new URL(urlString); HttpsURLConnection urlConnection = (HttpsURLConnection)url.openConnection(); urlConnection.setSSLSocketFactory(context.getSocketFactory()); return urlConnection; } catch (Exception ex) { Log.e(TAG, "Failed to establish SSL connection to server: " + ex.toString()); return null; } }
Good luck! And you’re welcome.
P.S. 20 september 2019: For some reason the solution above is insufficient on Android 28 and 29 (Pie and Q). On the emulators I tried it required an extra bit of code just before the return urlConnection
:
// 20 september 2019: // For some reason this call to setHostnameVerifier() has become necessary in // Android 28 (also doesn't work in 29). // In older Androids everything works fine without this. urlConnection.setHostnameVerifier(new HostnameVerifier() { @Override public boolean verify(String hostname, SSLSession session) { if (hostname.equalsIgnoreCase("littlesvr.ca")) { return true; } else { return false; } } });
I don’t know why that extra requirement. My certificate is for only one host, and it’s the host I’m connecting to. It even has reverse DNS set up.