SSL pinning

A certificate authority (CA) is needed to solve the key distribution problem in regular network clients, such as web browsers, IM, and e-mail clients. They need to communicate with many servers, which the application developers have no prior knowledge of. As we have discussed in the previous recipes, it's common to know the backend servers or services your app is communicating with, and so it is advisable to restrict the other CA roots.

Android currently trusts around 130 CAs, varying slightly between manufacturers and versions. It also restricts other CA roots and enhances the security of the connection. If one of these CAs were to be compromised, an attacker could use the compromised CA's root certificate to sign and issue new certificates for our server's domain. In this scenario, the attacker could complete a MITM attack on our app. This is because the standard HTTPS client validation will recognize the new certificates as trusted.

SSL pinning is one way to restrict who is trusted, and is usually approached in the following two ways:

  • Certificate pinning
  • Public key pinning

Much like what we achieved in the Validating self-signed SSL certificates recipe of this chapter, certificate pinning limits the number of trusted certificates to the ones in a local truststore. When using a CA, you would include your server's SSL certificate plus the root signing of the certificate and any intermediary certificates into your local truststore. This allows the full validation of the whole certificate chain; so when a compromised CA signs new certificates, these would fail the local truststore verification.

Public key pinning follows the same idea but is slightly more difficult to implement. There is an additional step of extracting the public key from the SSL certificate rather than just bundling the certificate(s) in the app. However, the extra effort is worth it because public keys remain consistent between certificate renewals. This means there is no need to force users to upgrade the app when the SSL certificate has been renewed.

In this recipe, we are going to pin against several certificate public keys using Android.com as an example. The recipe consists of two distinct parts; the first is a standalone Java utility to process and get the public keys from all of the SSL certificates in the chain and convert them to SHA1 hashes to embed/pin in your app. We embed SHA1 hashes of the public keys, as it is more secure.

The second part deals with the app code and how to verify the pins at runtime, and to decide whether a particular SSL connection is to be trusted.

How to do it...

Let's get started!

  1. We're going to create a standalone Java file called CalcPins.java that we will run on the command line to connect and print the SHA1 hashes of the certificate public keys. As we are dealing with a certificate signed by CA, there will be two or more certificates in the chain. This first step is mostly initiation and code to get the arguments to pass to the fetchAndPrintPinHashs method:
    public class CalcPins {
    
      private MessageDigest digest;
    
      public CalcPins() throws Exception {
        digest = MessageDigest.getInstance("SHA1");
      }
      
      
      public static void main(String[] args) {
        if ((args.length == 1) || (args.length == 2)) {
          String[] hostAndPort = args[0].split(":");
          String host = hostAndPort[0];
          // if port blank assume 443
          int port = (hostAndPort.length == 1) ? 443 : Integer
              .parseInt(hostAndPort[1]);
    
          try {
            CalcPins calc = new CalcPins();
            calc.fetchAndPrintPinHashs(host, port);
          } catch (Exception e) {
            e.printStackTrace();
          }
        } else {
          System.out.println("Usage: java CalcPins <host>[:port]");
          return;
        }
      }
  2. Next, we define the PublicKeyExtractingTrustManager class, which actually does the extraction of the public keys. The checkServerTrusted method will be called with the full chain of X509Certificates, when the socket connects, which is shown in a later step. We take the chain (the X509Certificate[] array) and call cert.getPublicKey().getEncoded(); to get a byte array for each public key. We then use the MessageDigest class to compute the SHA1 hash of the key. As this is a simple console application, we print the SHA1 hash to System.out:
    public class PublicKeyExtractingTrustManager implements X509TrustManager {
    
        public X509Certificate[] getAcceptedIssuers() {
          throw new UnsupportedOperationException();
        }
    
        public void checkClientTrusted(X509Certificate[] chain, String authType)
            throws CertificateException {
          throw new UnsupportedOperationException();
        }
    
        public void checkServerTrusted(X509Certificate[] chain, String authType)
            throws CertificateException {
          for (X509Certificate cert : chain) {
            byte[] pubKey = cert.getPublicKey().getEncoded();
            final byte[] hash = digest.digest(pubKey);
            System.out.println(bytesToHex(hash));
          }
        }
      }
  3. Then, we write the bytesToHex() utility method as follows:
    public static String bytesToHex(byte[] bytes) {
        final char[] hexArray = { '0', '1', '2', '3', '4', '5', '6', '7', '8','9', 'A', 'B', 'C', 'D', 'E', 'F' };
        char[] hexChars = new char[bytes.length * 2];
        int v;
        for (int j = 0; j < bytes.length; j++) {
          v = bytes[j] & 0xFF;
          hexChars[j * 2] = hexArray[v >>> 4];
          hexChars[j * 2 + 1] = hexArray[v & 0x0F];
        }
        return new String(hexChars);
      }

    We use a utility method to convert the byte array into upper case hexadecimal string before printing to System.out so that they can be embedded into our Android app.

  4. Finally, we use the host and port that was passed from the main method to open a SSLSocket connection to the host:
    private void fetchAndPrintPinHashs(String host, int port) throws Exception {
        SSLContext context = SSLContext.getInstance("TLS");
        PublicKeyExtractingTrustManager tm = new PublicKeyExtractingTrustManager();
        context.init(null, new TrustManager[] { tm }, null);
        SSLSocketFactory factory = context.getSocketFactory();
        SSLSocket socket = (SSLSocket) factory.createSocket(host, port);
        socket.setSoTimeout(10000);
        socket.startHandshake();
        socket.close();
      }

    We initialize the SSLContext object with our custom PublicKeyExtractingTrustManager class, which in turn prints the public key hash of each certification to the console ready for embedding in the Android app.

  5. From the terminal window, compile CalcPins.java with the javac and run commands using java with hostname:port as a command-line argument. The sample uses Android.com as an example host:
    $ javac CalcPins.java 
    $ java -cp . CalcPins Android.com:443
    

    However, you might find it easier to create CalcPins.java as a simple Java project in your IDE then export it as a runnable .jar file.

    A sample terminal command for the runnable .jar is as follows:

    $ java -jar calcpins.jar android.com:443
    

    If the public key extraction works, you will see the hash's output. This sample output shows the pins of three SSL certificate public keys of the Android.com host:

    B3A3B5195E7C0D39B8FA68D41A64780F79FD4EE9
    43DAD630EE53F8A980CA6EFD85F46AA37990E0EA
    C07A98688D89FBAB05640C117DAA7D65B8CACC4E
    

    Now, we move on to the second part of the recipe to verify the SSL connection in our Android app project.

  6. Now that we have the pins, we copy them from the terminal and embed them in a String array:
    private static String[] pins = new String[] {
          "B3A3B5195E7C0D39B8FA68D41A64780F79FD4EE9",
          "43DAD630EE53F8A980CA6EFD85F46AA37990E0EA",
          "C07A98688D89FBAB05640C117DAA7D65B8CACC4E" };
  7. Implement a custom TrustManager class that validates the pins:
    public class PubKeyPinningTrustManager implements X509TrustManager {
    
      private final String[] mPins;
      private final MessageDigest mDigest;
    
      public PubKeyPinningTrustManager(String[] pins)
          throws GeneralSecurityException {
        this.mPins = pins;
        mDigest = MessageDigest.getInstance("SHA1");
      }
    
      @Override
      public void checkServerTrusted(X509Certificate[] chain, String authType)
          throws CertificateException {
        // validate all the pins
        for (X509Certificate cert : chain) {
          final boolean expected = validateCertificatePin(cert);
          if (!expected) {
            throw new CertificateException("could not find a validpin");
          }
        }
      }
    
      @Override
      public void checkClientTrusted(X509Certificate[] chain, String authType)
          throws CertificateException {
        // we are validated the server and so this is not implemented.
        throw new CertificateException("Cilent valdation not implemented");
      }
    
      @Override
      public X509Certificate[] getAcceptedIssuers() {
        return null;
      }

    The PubKeyPinningTrustManager constructor is constructed with the pins array to use internally for validation. An instance of MessageDigest is also created to generate SHA1 hashes of incoming SSL certificate public keys. Note, for this example, that we are not implementing the checkClientTrusted() or getAcceptedIssuers() methods; see the Enhancements section.

  8. Validate the certificate:
    private boolean validateCertificatePin(X509Certificate certificate)
          throws CertificateException {
        final byte[] pubKeyInfo = certificate.getPublicKey().getEncoded();
        final byte[] pin = mDigest.digest(pubKeyInfo);
        final String pinAsHex = bytesToHex(pin);
        for (String validPin : mPins) {
          if (validPin.equalsIgnoreCase(pinAsHex)) {
            return true;
          }
        }
        return false;
      }

    We extract the public key and compute the SHA1 hash and then convert to a hexadecimal string using the bytesToHex() method as noted previously. The validation then boils down to a simple String.isEquals operation (actually, we use equalsIgnoreCase just in case there is a case mismatch). If the pin from the certificate does not match one of the embedded pins, a CertificateException is thrown and the connection will not be permitted.

  9. We can integrate PubKeyPinningTrustManager in the same way as the LocalTrustStoreTrustManager class, discussed earlier in this chapter. Here is an example of this being used with HttpsURLConnection:
    TrustManager[] trustManagers = new TrustManager[] { new PubKeyPinningTrustManager(pins) };
        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(null, trustManagers, null);
        HttpsURLConnection urlConnection = (HttpsURLConnection)url.openConnection();
        urlConnection.setSSLSocketFactory(sslContext.getSocketFactory());
        urlConnection.connect();

In conclusion, we extracted the certificate public keys and generated SHA1 hashes to embed in our app. Use these at runtime to validate the public keys of the SSL certificates of the SSL connection. This not only protects against other CAs being compromised, but also makes things more difficult for MITM attackers. The great thing is that we are using the industry standard SSL infrastructure, just in a stringent way.

There's more...

It is important to understand where this recipe can be improved and where the limitations are.

Enhancements

For maximum security, each time you make a server connection, you should validate the SSL pins. However, there is a trade off with performance per connection; therefore, you could adapt the previous code to check the first couple of connections per session. Although, this obviously comprises security. Also, including the Android's default trust manager validation would further increase the security. An open source library called AndroidPinning by Moxie Marlinspike has these enhancements implemented. You could also change the hash algorithm to a stronger version of SHA.

The validateCertificatePin method is an ideal candidate for DexGuard's API hiding, as mentioned in Chapter 5, Protecting Applications.

Limitations

While SSL pinning makes it more difficult for MITM attackers, it's not a 100 percent solution (not that any security solution is 100 percent). There is an interesting library from iSECPartners, which aims to circumvent pinning (https://github.com/iSECPartners/android-ssl-bypass).

However, the anti-temper recipes noted in Chapter 5, Protecting Applications, could be used to mitigate the .apk modification and the ability to run on an emulator.

See also

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

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