Tuesday, January 22, 2013

Android HttpsUrlConnection, self-signed certificate and hostname verifier

Today, an Android hint. Note for myself for future :)

So, what we have:

  1. Server
    REST (JAX-RS+akka), published to Tomcat. Tomcat serves app via HTTPS having a self-signed certificate (official tomcat docs).
    No CA, no chain - just a single certificate done in a really usual way.
    Tomcat is published on a box accessible only via IP, no DNS here.
    The actual server is https://46.4.224.49 just in case someone wants to take a look @cert.
  2. Client
    Android, trying to talk to the server above in JSON. Should use HTTPS for both traffic encryption and identity confirmation.
Generally, implementation seems pretty straightforward - get the public certificate, and make Android https facilities to trust it. 

I decided to use HttpsUrlConnection rather than HttpClient as the documentation suggest the former was only preferred for small operations in Android versions prior to 3. Trick is that Android is not shipped with the JKS keystore implementation which seems to belong to Oracle (Sun before).

After some googling I combined 3 articles to make a good working example:
  1. http://blog.crazybob.org/2010/02/android-trusting-ssl-certificates.html
    use first 2 steps, how to obtain public certificate off the server, pack it to the BKS keystore and some clues on how to setup BouncyCastle
    (hint - as of early 2013 we need version bcprov-jdk15on-146.jar, though newer is there)
    Added correct line to jre/lib/security/java.security to support another one provider, and dropped the jar into jre/lib/ext. This is only needed for keytool to understand new format.
  2. http://developer.android.com/reference/javax/net/ssl/HttpsURLConnection.html
    Feed HttpsUrlConnection with the SslSocketFactory relying on that keystore. Keystore is put into the res/raw and loaded via context.getResources().openRawResource(int id)
And this is where the trouble started. The test code was producing error. After getting the certificate in the error has changed from 'Not trusted certificate' to 'Can not verify hostname'. Which is progress. 

The very major of the solutions on the Internet were just allowing all the hostname, and even deeper, accepting all the certificates -- which was inacceptable due to security reason.

The certificate had an IP address in CN field. After some research, it was clear that certificate is there, it is trusted, however hostname check fails. That's when DefaultHostnameVerifier was spotted (yes i know javadoc is old, but debugger shows this very class, which is hidden in library-no source). However, old javadoc says clearly that all the checks are declined!

Surely we can not have this check passed as nothing is allowed. 

The issue is resolved easily with setting conn.setHostnameVerifier(new BrowserCompatHostnameVerifier()) - this triggers HttpsUrlConnection to the same mode as the browser (passing *.domain CNs, and IP addresses, etc), which is perfectly OK for our case. 
 

4 comments:

msb said...

Hi Alexander,

First off, thank you for your post and your insightful suggestions for good TLS security on Android. No one else doing android dev on the internet seems to understand the problems and points you've made here.

Stack overflow seems to be filled with suggestions to write custom trust managers which accept every certificate, which completely defeats the point of certificate-based authentication, as you point out.

After finally getting a self-signed CA (_not_ a host certificate) installed on my emulator and test device (after following completely wrong instructions here: https://coderwall.com/p/wv6fpq), I then encountered the problem you wrote about here, which is elegantly solved by you.

I now have full encryption and certificate-based authentication, which is as close the production server as I can get.

большое спасибо!

~Matthew

Alexander said...

Hey Matthew, thanks for the good words!

Happy to see this postings are drawing some attention; I was really writing them 'for myself' simply not to hit the same issue again and use blog as a reference. On a better note, though, looks like no one is really caring about the authenticity really. Main goal of everyone using HTTPS is rather getting the encryption, for which the allow-all policy is, well, acceptable.


You are welcome!

Just in case that's my GH account - I am following with all the public stuff there now, using blog only occasionally.

Alex.

Alex said...

I cannot thank you enough... Why this is not on the android developer page on ssl is beyond me.

http://developer.android.com/training/articles/security-ssl.html

Also, I agree with the other poster about all the hackish solutions on SO. Thanks again for posting.

Sevin said...

Hey Alex, thank you for your post! I encountered the same problem and I tried to use your method. Unfortunately, during my time of coding, the class BrowserCompatHostnameVerifier() was already deprecated. After some research, I discovered a new solution which can be found in the Android Developer website : http://developer.android.com/training/articles/security-ssl.html#CommonHostnameProbs

Inside this method :

HostnameVerifier hostnameVerifier = new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
HostnameVerifier hv =
HttpsURLConnection.getDefaultHostnameVerifier();
return hv.verify("example.com", session);
}
};

You just need to change "example.com" to "CN of cert" whereby CN of cert is the common name of the certificate that you are using. Hope it helps! =)