Validating self-signed SSL certificates

Android supports the use of SSL with standard Android API components, such as HTTPClient and URLConnection. However, if you attempt to connect to a secure HTTPS server URL, you may encounter an SSLHandshakeException. The common issues are:

  • The certificate authority (CA) who issued the server SSL certificate is not included in the ~130 CAs that are included as part of the Android system, and therefore, is treated as unknown
  • The server SSL certificate is self signed
  • The server isn't configured with intermediary SSL certificates

If the server isn't configured with intermediary certificates, it's simply a case of installing them to allow the connection code to validate the root of trust. However, if the server is using a self-signed certification or a CA-issued certificate but the CA isn't trusted by Android, we need to customize the SSL validation.

A common practice is to develop and test with servers that have self-signed SSL certificates and only use paid CA-signed certificates in the live environment. Therefore, this recipe specifically focuses on robustly validating self-signed SSL certificates.

Getting ready

For this recipe, we will be importing the self-signed SSL certificate into the app, and to do this, we are going to run some terminal commands. This section will cover the tools and commands to download the SSL certificate files on your machine.

The latest version of the Bouncy Castle library is needed later in this recipe to create and import certificates into the truststore. We use Bouncy Castle as it is a robust open source cryptology library that Android has built-in support for. You'll find the bcprov.jar file at http://www.bouncycastle.org/latest_releases.html. Download and save it to the current working directory. For this recipe, we have saved it to a local directory called libs so the path to reference the .jar file is /libs/bcprov-jdk15on-149.jar (which is the latest version at the time of writing this book).

We will need a self-signed SSL certificate file from the server; if you created yours manually or already have it, you can skip the rest of this section and move on to the recipe.

To create or download an SSL certificate, we will need to take advantage of the open source SSL toolkit known as OpenSSL:

  • Mac – Fortunately, OpenSSL has been included on Mac OS X since Version 10.2.
  • Linux – Many Linux distributions come with precompiled OpenSSL packages installed. If not, download and build the source code from https://www.openssl.org/source/or if you are on Ubuntu, it should be a case of apt-get install openssl.
  • Windows – Build from source or use a third-party-provided Win32 installer from Shining Light Productions (http://slproweb.com/products/Win32OpenSSL.html).

To get the certificates from the server in the terminal window, type the following command, where server.domain is either the IP address or server name:

Openssl s_client -showcerts -connect server.domain:443 </dev/null.

The certificate details will be displayed in the console output. Copy and paste the certificate that is defined, starting with -----BEGIN CERTIFICATE----- and ending with -----END CERTIFICATE-----, into a new file and save it as mycert.crt. It's important not to include any additional white space or trailing spaces.

The following screenshot shows an example of the Openssl –showcerts command for android.com:

Getting ready

If you don't have a server yet and want to create a new self-signed certificate to use, we first need to generate a private RSA key using the OpenSSL toolkit. Type the following into a terminal window:

openssl genrsa –out my_private_key.pem 2048

This creates the private key file my_private_key.pem. The next step is to generate the certificate file using the private key generated in the previous step. In the terminal, type:

openssl req -new -x509 -key my_private_key.pem -out mycert.crt -days 365

Follow the onscreen prompts and fill in the certificate details. Note the common name is typically your server IP address or domain name.

That's it for getting ready! We should have a certificate file in hand for the next section.

How to do it...

Let's get started!

  1. You should have an SSL certificate in CRT/PEM encoded format, which when opened in, text editor, looks something like this:
    -----BEGIN CERTIFICATE-----
    WgAwIBAgIDA1MHMA0GCSqGSIb3DQEBBQUAMDwxCzAJBgNVBAYTAlVTMRcwFQYDVQQK
    …
    -----END CERTIFICATE-----

    For this recipe, we will use the example named mycert.crt.

  2. To package the certificates into an app, we create and import the certificates into a .keystore file that we will refer to as our app's truststore.
  3. In a terminal window, set the CLASSPATH variable so that the following command can access the bcprov.jar file:
    $export CLASSPATH=libs/bcprov-jdk15on-149.jar
    

    The preceding command path of the bcprov-jdk15on-149.jar file should match the -providerpath argument.

  4. Now, create and import the certificate with the following keytool command:
    $ keytool -import -v -trustcacerts -alias 0 /
    -file <(openssl x509 -in mycert.crt) /
    -keystore customtruststore.bks /
    -storetype BKS /
    -providerclass org.bouncycastle.jce.provider.BouncyCastleProvider /
    -providerpath libs/bcprov-jdk15on-149.jar 
    -storepass androidcookbook 
    
  5. You should be prompted to trust the certificate, type yes:
    Trust this certificate? [no]: yes
    

    The output file is customtruststore.bks, with the public certificate added. The truststore is protected with a password, androidcookbook, which we will reference in the code when we load the truststore in the app. We set the –storetype argument as BKS, which denotes the Bouncy Castle Keystore type, also explaining the .bks extension. It's possible to import multiple certificates into your truststore; for example, development and test servers.

    Tip

    Difference between keystore and truststore

    Although they are the same type of file (.keystore), and in fact can be the same file, we tend to have separate files. We use the term truststore to define a set of third-party public certificates you expect to communicate with. Whereas, a keystore is for private keys and should be stored in a protected location (that is, not in the app).

  6. Copy the truststore file into the raw folder of your Android app; if the folder doesn't exist, create it:

    /res/raw/customtruststore.bks

  7. Load the local truststore from the raw directory into a KeyStore object:
    private static final String STORE_PASSWORD = "androidcookbook";
    
    private KeyStore loadKeyStore() throws Exception {
        final KeyStore keyStore = KeyStore.getInstance("BKS");
        final InputStream inputStream = context.getResources().openRawResource(
            R.raw.customtruststore);
        try {
          keyStore.load(inputStream, STORE_PASSWORD.toCharArray());
          return keyStore;
        } finally {
          inputStream.close();
        }
      }

    Here, we create an instance of the KeyStore class with the type BKS (Bouncy Castle Keystore) that matches the type we created. Conveniently, there is a .load() method, which takes the input stream (InputStream) of the loaded .bks file. You'll notice we are using the same password we used to create the truststore to open, verify, and read the contents. The primary use of the password is to verify the integrity of the truststore rather than enforce security. Especially since the truststore contains the server's public certificate, it is not a security issue having this hardcoded, as the certificates are easily accessible from the URL. However, to make things harder for attackers, it could be a good candidate for DexGuard's string encryption as mentioned in Chapter 5, Protecting Applications.

  8. Extend DefaultHttpClient to use the local truststore:
    public class LocalTrustStoreMyHttpClient extends DefaultHttpClient {
    
        @Override
        protected ClientConnectionManager createClientConnectionManager() {
          SchemeRegistry registry = new SchemeRegistry();
          registry.register(new Scheme("http", PlainSocketFactory
              .getSocketFactory(), 80));
          try {
            registry.register(new Scheme("https", new SSLSocketFactory(
                loadKeyStore()), 443));
          } catch (Exception e) {
            e.printStackTrace();
          }
          return new SingleClientConnManager(getParams(), registry);
        }
      }

    We override the createClientConnectionManager method so that we can register a new SSLSocketFactory interface with our local truststore. For brevity of the code samples, here we have caught the exception and printed the error to the system log; however, it is recommended to implement appropriate error handling and reduce the amount of information logged when using this in live code.

  9. Write a sample HTTP GET request using HttpClient:
      public HttpResponse httpClientRequestUsingLocalKeystore(Stringurl)
          throws ClientProtocolException, IOException {
        HttpClient httpClient = new MyHttpClient();
        HttpGet httpGet = new HttpGet(url);
        HttpResponse response = httpClient.execute(httpGet);
        return response;
      }

    This shows us how to construct a simple HTTP GET request and use the LocalTrustStoreMyHttpClient class, which doesn't throw SSLHandshakeException because the self-signed certificate from the server can be successfully verified.

Tip

Gotcha

We have defined an explicit truststore for all HTTPS requests. Remember, if the backend server certificate is changed, the app will cease to trust the connection and throw SecurityException.

That concludes this recipe; we can communicate with Internet resources that are protected by SSL and signed with our self-signed SSL certificate.

There's more...

In general, when dealing with SSL, a common mistake is to catch and hide certificate and security exceptions. This is exactly what an attacker is relying on to dupe an unsuspecting app user. What you choose to do about SSL errors is subjective and depends on the app. However, blocking networking communications is usually a good step to ensure that data is not transmitted over a potentially compromised channel.

Using self-signed SSL certificates in a live environment

It is common for Android application developers to know at compile/build time the servers they are commutating with. They may even have control over them. If you follow the validation steps noted here, there's no security issue with using self-signed certificates in a live environment. The advantage is that you'll insulate yourself from certificate authority compromise and save money of SSL certificate renewal fees.

HttpsUrlConnection

There's no additional security benefit, but you may prefer using the HttpsURLConnection API. For this, we take a slightly different approach and create a custom TrustManager class, which verifiers our local truststore file:

  1. Create a custom TrustManager class:
    public class LocalTrustStoreTrustManager implements X509TrustManager {
    
      private X509TrustManager mTrustManager;
    
      public LocalTrustStoreTrustManager(KeyStore localTrustStore) {
        try {
          TrustManagerFactory factory = TrustManagerFactory
              .getInstance(TrustManagerFactory.getDefaultAlgorithm());
          factory.init(localTrustStore);
    
          mTrustManager = findX509TrustManager(factory);
          if (mTrustManager == null) {
            throw new IllegalStateException(
                "Couldn't find X509TrustManager");
          }
        } catch (GeneralSecurityException e) {
          throw new RuntimeException(e);
        }
      }
    
      @Override
      public void checkClientTrusted(X509Certificate[] chain, String authType)
          throws CertificateException {
        mTrustManager.checkClientTrusted(chain, authType);
      }
    
      @Override
      public void checkServerTrusted(X509Certificate[] chain, String authType)
          throws CertificateException {
        mTrustManager.checkServerTrusted(chain, authType);
      }
    
      @Override
      public X509Certificate[] getAcceptedIssuers() {
        return mTrustManager.getAcceptedIssuers();
      }
    
      private X509TrustManager findX509TrustManager(TrustManagerFactory tmf) {
        TrustManager trustManagers[] = tmf.getTrustManagers();
        for (int i = 0; i < trustManagers.length; i++) {
          if (trustManagers[i] instanceof X509TrustManager) {
            return (X509TrustManager) trustManagers[i];
          }
        }
        return null;
      }
    
    }

    We implement the X509TrustManager interface, and the constructor of our LocalTrustStoreTrustManager class takes a KeyStore object, which we loaded in a previous step defined earlier in the recipe. As previously noted, this KeyStore object is referred to as our truststore because it contains the certificate we trust. We initialize the TrustManagerFactory class with the truststore and then using the findX509TrustManager() method, we get the system-specific implementation of the X509TrustManager interface. We then keep a reference to this TrustManager, which uses our truststore to verify whether a certificate from a connection is trusted, rather than using the system truststore.

  2. Here is an example of an HTTP GET request using HttpsURLConnection and the custom TrustManager class created in the previous step:
      public InputStream uRLConnectionRequestLocalTruststore(String targetUrl)
          throws Exception {
        URL url = new URL(targetUrl);
    
        SSLContext sc = SSLContext.getInstance("TLS");
        sc.init(null, new TrustManager[] { new LocalTrustStoreTrustManager(
            loadKeyStore()) }, new SecureRandom());
        HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
    
        HttpsURLConnection urlHttpsConnection = (HttpsURLConnection) url.openConnection();
        urlHttpsConnection.setRequestMethod("GET");
        urlHttpsConnection.connect();
        return urlHttpsConnection.getInputStream();
      }

    We initialize the SSLContext with the LocalTrustStoreTrustManager class so that when we call sc.getSocketFactory(), it will use our TrustManager implementation. This is set on the HttpsURLConnection by overriding the default using setDefaultSSLSocketFactory(). That's all you need to successfully connect to our self-signed SSL resources with URLConnection.

Antipattern – what not to do!

This is an antipattern that unfortunately is posted on various forums and message boards when developers are trying to work with self-signed certifications or SSL certificates signed by an untrusted certification authority.

Here, we see an insecure implementation of the X509TrustManager interface:

public class TrustAllX509TrustManager implements X509TrustManager {

  @Override
  public void checkClientTrusted(X509Certificate[] chain, String authType)
      throws CertificateException {
    // do nothing, trust all :(
  }

  @Override
  public void checkServerTrusted(X509Certificate[] chain, String authType)
      throws CertificateException {
    // do nothing, trust all :( 
  }

  @Override
  public X509Certificate[] getAcceptedIssuers() {
    return null;
  }
}

As you can see from the code, the checkServerTrusted method has no validation implemented consequently, and all servers are trusted. This leaves HTTPS communications exposed to a man-in-the-middle (MITM) attack, which defeats the whole point of using certificates.

See also

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.226.104.202