Arrow
Blog Bookmark Icon

  • Blog >>
  • Guardian's StrongTrustManager Vulnerabilities

Mar 19, 2013

Last week I saw a tweet about Guardian Project’s “StrongTrustManager,” which was built for increasing the security of SSL connections in Android. It’s part of their OnionKit library, and their app Gibberbot uses it to secure its XMPP connections.

I recently released an Android library that provides simple SSL pinning support, and have previously written about the great opportunity we have for mobile apps to sidestep the many problems plaguing us with CA certificates, so I was excited to see something else out there.

Since I had just released something similar, I went to look at what the Guardian Project implementation provides, and incidentally ended up discovering a few security vulnerabilities. I’ve decided to write them up here, since they’ve turned out to be fairly common problems amongst TLS implementations, and might be of some value to document.

Danger

The first thing I immediately noticed was that StrongTrustManager completely replaces all certificate validation done by the Android framework. Rather than using the Android framework to validate SSL certificate chains as usual, and then doing additional checks beyond that, the StrongTrustManager was taking full responsibility for everything.

This is a really big risk. I’m not actually a supporter of the general adage “never roll your own crypto.” I believe that cryptography is a fairly closed system, and that it’s relatively straightforward to learn how to carefully use cryptographic primitives to build protocols securely. Certificate validation, on the other hand, is something that I would recommend people avoid doing themselves, if possible. It’s mired in cruft and gotchas.

StrongTrustManager trips over a few of these gotchas, resulting in a few different vectors for simple MITM attacks.

No BasicConstraints Verification

This is the same bug that I originally reported against all web browsers back in 2002. The StrongTrustManager implementation fails to verify that the intermediate certificates in a certificate chain are valid for CA signing, and only checks that the root is trusted instead.

Let’s say that VeriSign is a trusted root, and I wish to intercept traffic for secureservice.com. All I need to do is get a valid certificate for a domain that I own, such as thoughtcrime.org. The certificate chain will look like this:

VeriSign
|
+--thoughtcrime.org

Now I create another certificate for secureservice.com, and sign it with my legitimate CA-signed certificate for thoughtcrime.org:

VeriSign
|
+--thoughtcrime.org
   |
   +--secureservice.com

If I present this to StrongTrustManager, it will verify the signature chain up to a trusted root, and accept it as valid for secureservice.com. It is simple for an attacker to use a tool like sslsniff to intercept all encrypted traffic to secureservice.com.

StrongTrustManager also doesn’t check KeyUsage fields or any other critical extensions.

Insecure Signature Validation

We’ve seen how StrongTrustManager is vulnerable to a MITM attack by failing to validate BasicConstraints, but what about the situation where the trust root is “closed?” This would be the case if you have your own signing certificate for your service which you distribute with your app, rather than using CA certificates (which is how you should be doing it).

In this case, the above attack would be difficult, because there would be no way for us to get a legitimate certificate for our own domain signed by the private root. Unfortunately, there’s another attack vector that lets us perform our interception.

The StrongTrustManager validation code starts like this:

for (int i = 0; i < x509Certificates.length; i++)
{
  X509Certificate x509certCurr = x509Certificates[i];
                
  X509Certificate x509issuer = null;
  boolean isRootCA = false;
                
  for (X509Certificate x509search : x509Certificates)
  {                                      
    if(checkSubjectMatchesIssuer(x509search.getSubjectX500Principal(),x509certCurr.getIssuerX500Principal()))                 
    {                                                          
      x509issuer = x509search;           
                        
      //now check if it is a root
      X509Certificate x509root = findCertIssuerInStore(x509certCurr, mTrustStore);
      if (x509root != null)
        isRootCA = true;
                        
      break;
    }
  }

  ....

}

For every certificate in the certificate chain, the code loops through the chain again from the beginning in order to find that certificate’s issuer. That presents the attacker with an interesting opportunity. Let’s say that a service is presenting the following certificate chain:

ClosedTrustedRoot
|
+--secureservice.com

The BasicConstraints attack doesn’t apply, because it’s a closed trusted root, so we can’t get a certificate of our own. But because of the way that issuers are found and verified, we could present this to the client:

ClosedTrustedRoot
|
+--secureservice.com (the original)

ToxicIssuer (issued by forged secureservice.com)
|
secureservice.com (issued by ToxicIssuer)

It’s the original certificate chain presented by the service, with two certificates tacked on to the front. The two certificates added to the front cross-sign each-other: each is the other’s issuer, and each is signed by the other. The StrongTrustManager code will evaluate the first certificate (our forged secureservice.com cert) and find its issuer (ToxicIssuer). It will check the signature and then move on to evaluating ToxicIssuer. When looking for its issuer, it will start at the beginning of the chain, and find our forged secureservice.com cert!

The code will verify the signature, then move on and evaluate the rest of the chain, stopping at the top to check the trust on ClosedTrustedRoot, which of course is trusted. The result is another MITM attack that could be deployed easily via sslsniff.

Ineffective Strengths

The “Strong” in StrongTrustManager is for providing enhanced protection beyond what the system TrustManager gives us. StrongTrustManager does this by focusing on public key pinning and weak hash detection.

The pinning code looks like this:

private void checkPinning (X509Certificate x509cert) throws CertificateException
{
        
  X500Principal certPrincipal = x509cert.getSubjectX500Principal();
        
  Enumeration<String> enumAliases;
  try {
    enumAliases = mPinnedStore.aliases();
    X509Certificate x509search = null;
    while (enumAliases.hasMoreElements()) {
      x509search = (X509Certificate) mPinnedStore
                      .getCertificate(enumAliases.nextElement());
                
      X500Principal searchPrincipal = x509search.getSubjectX500Principal();
                
      //name check
      if (certPrincipal.getName("RFC1779").equals(searchPrincipal.getName("RFC1779"))) 
      {
        //found matching pinned cert, now check if the certs are the same
        //byte by byte check
        if (!Arrays.equals(certPrincipal.getEncoded(), searchPrincipal.getEncoded())) 
        {

          if (mNotifyVerificationFail)
            showCertMessage(mContext.getString(R.string.warning_pinned_cert_mismatch),
                            x509cert.getSubjectDN().getName(), x509cert, null);
                        
          // just warn for now, don't block
          // throw new CertificateException(mContext.getString(R.string.warning_pinned_cert_mismatch) + certPrincipal.getName("RFC1779"));
                        
        }
                    
        break;
      }
    }
  } catch (KeyStoreException e) {
    Log.e(TAG, "problem access local keystore",e);
    throw new CertificateException("problem access local keystore");
  }
}

There is a problem and a vulnerability here. The problem is that this function is only called once, for the end-entity certificate. Until TACK exists, it is not possible to effectively pin an end-entity certificate. Since end-entity certificates commonly change, the idea behind pinning is to pin something in the signing chain. Otherwise we could just hardcode a self-signed certificate and be done with it.

The additional vulnerability is that pinned certs have to be matched by SPKI, which is why “public key pinning” is technically a more accurate term than “certificate pinning.” This code is comparing subject names until it finds a pinned certificate in its keystore with the same subject name, at which point it does a “byte by byte check” (as indicated by the comment). The byte-by-byte comparison, however, is only on the DER encoded Subject, rather than over anything with key material in it. To bypass pinning here, we’d only need to include a certificate with the same subject name as a certificate in the pin store, which would presumably be easy to do in any of the circumstances where pinning becomes of value.

There is a similar phenomenon with the weak hash algorithm detection:

private void checkStrongCrypto (X509Certificate cert) throws CertificateException
{
  String algo = cert.getSigAlgName().toLowerCase();
        
  if (algo.contains("md5"))
  {

    if (mNotifyVerificationFail)
      showCertMessage(mContext.getString(R.string.warning_weak_crypto),
                      cert.getIssuerDN().getName(), cert, null);

    throw new CertificateException("issuer uses weak crypto: " + algo);
  }
}

This function will signal a failure if a certificate is signed using MD5, but it’s only called once for the end-entity certificate, rather than for each certificate in the chain. Presumably if an attacker can forge a certificate with an MD5 hash collision, they could simply use that certificate to sign another certificate using SHA1 and bypass this check.

Conclusion

The result is a few different vectors for performing MITM attacks against Gibberbot, or other clients of OnionKit. Some of these are vulnerabilities that keep appearing, so be careful if you’re in a position where you are forced to do your own certificate validation.

If you’re interested in writing a more restrictive TrustManager implementation for Android, my recommendation is to have your implementation call through to the system’s default TrustManager implementation as the very first thing it does. That way you can ensure you at least won’t be doing any worse than the default, even if there are vulnerabilities in the additional checks you do.

I reported these problems to Guardian last week; today they notified me that they’ve finished addressing them and have shipped security fixes for Gibberbot and OnionKit. Users are advised to look for the update. I just looked at the fixes they developed for these vulnerabilities and noticed new vulnerabilities in the fixes. Practical MITM attacks are still present for Gibberbot and all clients of OnionKit.