Programmatically add a certificate authority while

2019-03-08 21:38发布

There are lots of questions about this topic on StackOverflow, but I do not seem to find one related to my problem.

I have an Android application that needs to communicate with HTTPS servers: some signed with a CA registered in the Android system keystore (common HTTPS websites), and some signed with a CA I own but not in the Android system keystore (a server with an autosigned certificate for instance).

I know how to add my CA programmatically and force every HTTPS connection to use it. I use the following code:

public class SslCertificateAuthority {

    public static void addCertificateAuthority(InputStream inputStream) {

        try {
            // Load CAs from an InputStream
            // (could be from a resource or ByteArrayInputStream or ...)
            CertificateFactory cf = CertificateFactory.getInstance("X.509");
            InputStream caInput = new BufferedInputStream(inputStream);
            Certificate ca;
            try {
                ca = cf.generateCertificate(caInput);
            } finally {
                caInput.close();
            }

            // 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
            HttpsURLConnection.setDefaultSSLSocketFactory(context.getSocketFactory());
        } catch (CertificateException e) {
            e.printStackTrace();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (KeyStoreException e) {
            e.printStackTrace();
        } catch (KeyManagementException e) {
            e.printStackTrace();
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }  

    }

}

However, doing that disables the use of the Android system keystore, and I cannot query HTTPS sites signed with other CA any more.

I tried to add my CA in the Android keystore, using:

KeyStore.getInstance("AndroidCAStore")

... but I cannot then add my CA in it (an exception is launched).

I could use the instance method HttpsURLConnection.setSSLSocketFactory(...) instead of the static global HttpsURLConnection.setDefaultSSLSocketFactory(...) to tell on a case by case basis when my CA has to be used.

But it isn't practical at all, all the more since sometimes I cannot pass a preconfigured HttpsURLConnection object to some libraries.

Some ideas about how I could do that?


EDIT - ANSWER

Ok, following the given advice, here is my working code. It might need some enhancements, but it seems to work as a starting point.

public class SslCertificateAuthority {

    private static class UnifiedTrustManager implements X509TrustManager {
        private X509TrustManager defaultTrustManager;
        private X509TrustManager localTrustManager;
        public UnifiedTrustManager(KeyStore localKeyStore) throws KeyStoreException {
            try {
                this.defaultTrustManager = createTrustManager(null);
                this.localTrustManager = createTrustManager(localKeyStore);
            } catch (NoSuchAlgorithmException e) {
                e.printStackTrace();
            }
        }
        private X509TrustManager createTrustManager(KeyStore store) throws NoSuchAlgorithmException, KeyStoreException {
            String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
            TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
            tmf.init((KeyStore) store);
            TrustManager[] trustManagers = tmf.getTrustManagers();
            return (X509TrustManager) trustManagers[0];
        }
        public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
            try {
                defaultTrustManager.checkServerTrusted(chain, authType);
            } catch (CertificateException ce) {
                localTrustManager.checkServerTrusted(chain, authType);
            }
        }
        @Override
        public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
            try {
                defaultTrustManager.checkClientTrusted(chain, authType);
            } catch (CertificateException ce) {
                localTrustManager.checkClientTrusted(chain, authType);
            }
        }
        @Override
        public X509Certificate[] getAcceptedIssuers() {
            X509Certificate[] first = defaultTrustManager.getAcceptedIssuers();
            X509Certificate[] second = localTrustManager.getAcceptedIssuers();
            X509Certificate[] result = Arrays.copyOf(first, first.length + second.length);
            System.arraycopy(second, 0, result, first.length, second.length);
            return result;
        }
    }

    public static void setCustomCertificateAuthority(InputStream inputStream) {

        try {
            // Load CAs from an InputStream
            // (could be from a resource or ByteArrayInputStream or ...)
            CertificateFactory cf = CertificateFactory.getInstance("X.509");
            InputStream caInput = new BufferedInputStream(inputStream);
            Certificate ca;
            try {
                ca = cf.generateCertificate(caInput);
                System.out.println("ca=" + ((X509Certificate) ca).getSubjectDN());
            } finally {
                caInput.close();
            }

            // 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 and system CA
            UnifiedTrustManager trustManager = new UnifiedTrustManager(keyStore);

            // Create an SSLContext that uses our TrustManager
            SSLContext context = SSLContext.getInstance("TLS");
            context.init(null, new TrustManager[]{trustManager}, null);

            // Tell the URLConnection to use a SocketFactory from our SSLContext
            HttpsURLConnection.setDefaultSSLSocketFactory(context.getSocketFactory());

        } catch (CertificateException e) {
            e.printStackTrace();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (KeyStoreException e) {
            e.printStackTrace();
        } catch (KeyManagementException e) {
            e.printStackTrace();
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

3条回答
爷、活的狠高调
2楼-- · 2019-03-08 21:58

It is an old question, but I met the same problem, so probably it is worth posting my answer. You tried to add your certificate to KeyStore.getInstance("AndroidCAStore"), but got an exception. Actually you should have done the opposite - add entries from that keystore to the one you created. My code is a bit different from yours, I just post it for the sake of complete answer even though only middle part matters.

KeyStore keyStore=KeyStore.getInstance("BKS");
InputStream in=activity.getResources().openRawResource(R.raw.my_ca);
try
{
  keyStore.load(in,"PASSWORD_HERE".toCharArray());
}
finally
{
  in.close();
}
KeyStore defaultCAs=KeyStore.getInstance("AndroidCAStore");
if(defaultCAs!=null)
{
  defaultCAs.load(null,null);
  Enumeration<String> keyAliases=defaultCAs.aliases();
  while(keyAliases.hasMoreElements())
  {
    String alias=keyAliases.nextElement();
    Certificate cert=defaultCAs.getCertificate(alias);
    try
    {
      if(!keyStore.containsAlias(alias))
        keyStore.setCertificateEntry(alias,cert);
    }
    catch(Exception e)
    {
      System.out.println("Error adding "+e);
    }
  }
}
TrustManagerFactory tmf=TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(keyStore);
// Get a new SSL context
SSLContext ctx = SSLContext.getInstance("SSL");
ctx.init(null,tmf.getTrustManagers(),new java.security.SecureRandom());
return ctx.getSocketFactory();
查看更多
姐就是有狂的资本
3楼-- · 2019-03-08 22:03

This might be too late but this is a tried and tested approach that helps bypass the certificate check done by Java.

I cannot claim credit for this code, it was written by one of my colleagues :) . It can be used during development to test your code. In case you don't want to deal with certificates at all, you can make Java always certificates from any host for your HttpURLConnection object. Which seems to be exactly what you're trying to do here.

Here's a class that should help you do that :

import javax.net.ssl.*;
import java.net.HttpURLConnection;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

/***
 * Should only be used in development, this class will allow connections to an HTTPS server with unverified certificates. 
 * obviously this should not be used in the real world
 */
public class TrustModifier {
private static final TrustingHostnameVerifier TRUSTING_HOSTNAME_VERIFIER = new TrustingHostnameVerifier();
private static SSLSocketFactory factory;

/**
 * Call this with any HttpURLConnection, and it will modify the trust settings if it is an HTTPS connection.
 *
 * @param conn the {@link HttpURLConnection} instance
 * @throws KeyManagementException   if an error occurs while initializing the context object for the TLS protocol
 * @throws NoSuchAlgorithmException if no Provider supports a TrustManagerFactorySpi implementation for the TLS protocol.
 */
public static void relaxHostChecking(HttpURLConnection conn) throws KeyManagementException, NoSuchAlgorithmException {
    if (conn instanceof HttpsURLConnection) {
        HttpsURLConnection httpsConnection = (HttpsURLConnection) conn;
        SSLSocketFactory factory = prepFactory();
        httpsConnection.setSSLSocketFactory(factory);
        httpsConnection.setHostnameVerifier(TRUSTING_HOSTNAME_VERIFIER);
    }
}

 /**
 * Returns an {@link SSLSocketFactory} instance for the protocol being passed, this represents a secure communication context
 *
 * @return a {@link SSLSocketFactory} object for the TLS protocol
 * @throws NoSuchAlgorithmException if no Provider supports a TrustManagerFactorySpi implementation for the specified protocol.
 * @throws KeyManagementException   if an error occurs while initializing the context object
 */
static synchronized SSLSocketFactory prepFactory() throws NoSuchAlgorithmException, KeyManagementException {
    if (factory == null) {
        SSLContext ctx = SSLContext.getInstance("TLS");
        ctx.init(null, new TrustManager[]{new AlwaysTrustManager()}, null);
        factory = ctx.getSocketFactory();
    }
    return factory;
}

private static final class TrustingHostnameVerifier implements HostnameVerifier {
    public boolean verify(String hostname, SSLSession session) {
        return true;
    }
}

private static class AlwaysTrustManager implements X509TrustManager {
    public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
    }

    public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
    }

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

All you need to do is call the function relaxHostChecking() like this :

    if (conn instanceof HttpsURLConnection) {
        TrustModifier.relaxHostChecking(conn);
    }

This will result in java trusting whichever host you're trying to connect to using HttpURLConnection.

查看更多
祖国的老花朵
4楼-- · 2019-03-08 22:04

Try to implement a custom trust manager, so that it checks your custom certificates and if this fails android builtin certificates.

Have a look at this article: Using a Custom Certificate Trust Store on Android.

I think the paragraph "Creating a dynamic TrustManager" handles exactly what you are asking.

查看更多
登录 后发表回答