在Android上使用客户端/服务器证书进行双向认证SSL套接字

44
我正在开发一个需要客户端和服务器证书认证的Android应用程序。我有一个SSLClient类,它在常规桌面Java SE 6上运行得非常好。我将其移入我的Android项目中,但出现了以下错误:“未找到KeyStore JKS实现”。
我在网上查找了一些信息,看起来可能不支持Java Keystores在Android上(太棒了!),但我觉得事情并不仅仅如此,因为我找到的所有示例代码都与我尝试做的事情完全不同。我找到的所有内容都是关于使用http客户端而不是原始的SSL套接字。我需要SSL套接字来实现这个应用程序。
下面是我的SSLClient.java文件中的代码。它读取密钥库和信任库,创建一个SSL套接字连接到服务器,然后在等待服务器输入行时运行循环,通过调用不同类中的方法来处理它们。我非常希望听到任何在Android平台上进行SSL套接字操作经验的人的意见。
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.security.AccessControlException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;

import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;
import otherpackege.OtherClass;

import android.content.Context;
import android.util.Log;

public class SSLClient 
{
    static SSLContext ssl_ctx;

    public SSLClient(Context context)
    {
        try
        {
            // Setup truststore
            KeyStore trustStore = KeyStore.getInstance("BKS");
            TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
            InputStream trustStoreStream = context.getResources().openRawResource(R.raw.mysrvtruststore);
            trustStore.load(trustStoreStream, "testtest".toCharArray());
            trustManagerFactory.init(trustStore);

            // Setup keystore
            KeyStore keyStore = KeyStore.getInstance("BKS");
            KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
            InputStream keyStoreStream = context.getResources().openRawResource(R.raw.clientkeystore);
keyStore.load(keyStoreStream, "testtest".toCharArray());
            keyManagerFactory.init(keyStore, "testtest".toCharArray());

            Log.d("SSL", "Key " + keyStore.size());
            Log.d("SSL", "Trust " + trustStore.size());

            // Setup the SSL context to use the truststore and keystore
            ssl_ctx = SSLContext.getInstance("TLS");
            ssl_ctx.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null);

            Log.d("SSL", "keyManagerFactory " + keyManagerFactory.getKeyManagers().length);
            Log.d("SSL", "trustManagerFactory " + trustManagerFactory.getTrustManagers().length);
        }
        catch (NoSuchAlgorithmException nsae)
        {
            Log.d("SSL", nsae.getMessage());
        }
        catch (KeyStoreException kse)
        {
            Log.d("SSL", kse.getMessage());
        }
        catch (IOException ioe)
        {
            Log.d("SSL", ioe.getMessage());
        }
        catch (CertificateException ce)
        {
            Log.d("SSL", ce.getMessage());
        }
        catch (KeyManagementException kme)
        {
            Log.d("SSL", kme.getMessage());
        }
        catch(AccessControlException ace)
        {
            Log.d("SSL", ace.getMessage());
        }
        catch(UnrecoverableKeyException uke)
        {
            Log.d("SSL", uke.getMessage());
        }

        try
        {
            Handler handler = new Handler();
            handler.start();
        }
        catch (IOException ioException) 
        {
            ioException.printStackTrace();
        }
     }  
}

//class Handler implements Runnable 
class Handler extends Thread
{
    private SSLSocket socket;
    private BufferedReader input;
    static public PrintWriter output;

    private String serverUrl = "174.61.103.206";
    private String serverPort = "6000";

    Handler(SSLSocket socket) throws IOException
    {

    }
    Handler() throws IOException
    {

    }

    public void sendMessagameInfoge(String message)
    {
        Handler.output.println(message);
    }

    @Override
    public void run() 
    {
        String line;

        try 
        {
            SSLSocketFactory socketFactory = (SSLSocketFactory) SSLClient.ssl_ctx.getSocketFactory();
            socket = (SSLSocket) socketFactory.createSocket(serverUrl, Integer.parseInt(serverPort));
            this.input = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            Handler.output = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()));
            Log.d("SSL", "Created the socket, input, and output!!");

            do
            {
                line = input.readLine();
                while (line == null)
                {
                    line = input.readLine();
                }

                // Parse the message and do something with it
                // Done in a different class
                OtherClass.parseMessageString(line);
            }
            while ( !line.equals("exit|") );
        }
        catch (IOException ioe)
        {
            System.out.println(ioe);
        }
        finally 
        {
            try 
            {
                input.close();
                output.close();
                socket.close();
            } 
            catch(IOException ioe) 
            {
            } 
            finally 
            {

            }
        }
    }
}

更新:
在解决这个问题上取得了一些进展。发现JKS确实不被支持,直接选择SunX509类型也不行。我已经更新了上面的代码以反映这些更改。但似乎仍存在一个问题,即似乎无法加载密钥库和信任库。我会随着进一步的了解进行更新。


更新2:
我以桌面Java的方式加载我的密钥库和信任库文件,而不是正确的Android方式。这些文件必须放在res/raw文件夹中,并使用getResources()进行加载。现在我得到了1个密钥库和1个信任库的大小计数,这意味着它们已经被加载。我仍然会因为异常而崩溃,但离成功更近了!当我解决问题后我会进行更新。


更新3:
现在看起来一切都正常了,除了我的密钥库设置不正确。如果我在服务器上禁用客户端身份验证,它会无问题地连接。当我启用它时,我会得到一个handling exception: javax.net.ssl.SSLHandshakeException: null cert chain错误。因此,看起来我没有正确设置证书链。我已经发布了另一个问题,询问如何创建包含客户端证书链的BKS(BouncyCastle)格式Java密钥库:如何创建包含客户端证书链的BKS(BouncyCastle)格式Java密钥库

1个回答

47

Android支持BKS、P12及其他格式的证书。

对于BKS格式: 使用Portecle将您的证书(.p12和.crt)转换为.bks格式。

/res/raw文件夹中,您需要两个文件: truststore.bks - 用于服务器的信任证书(从.cer文件转换而来) client.bks/client.p12 - 客户端证书(从包含客户端证书和客户端密钥的.p12文件转换而来)

import java.io.*;
import java.security.KeyStore;

import javax.net.ssl.*;

import org.apache.http.*;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.params.HttpClientParams;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.params.*;
import org.apache.http.conn.scheme.*;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.params.*;

import android.app.Activity;
import android.os.Bundle;

public class SslTestActivity extends Activity {

  /** Called when the activity is first created. */
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    try {
      // setup truststore to provide trust for the server certificate

      // load truststore certificate
      InputStream clientTruststoreIs = getResources().openRawResource(R.raw.truststore);
      KeyStore trustStore = null;
      trustStore = KeyStore.getInstance("BKS");
      trustStore.load(clientTruststoreIs, "MyPassword".toCharArray());

      System.out.println("Loaded server certificates: " + trustStore.size());

      // initialize trust manager factory with the read truststore
      TrustManagerFactory trustManagerFactory = null;
      trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
      trustManagerFactory.init(trustStore);

      // setup client certificate

      // load client certificate
      InputStream keyStoreStream = getResources().openRawResource(R.raw.client);
      KeyStore keyStore = null;
      keyStore = KeyStore.getInstance("BKS");
      keyStore.load(keyStoreStream, "MyPassword".toCharArray());

      System.out.println("Loaded client certificates: " + keyStore.size());

      // initialize key manager factory with the read client certificate
      KeyManagerFactory keyManagerFactory = null;
      keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
      keyManagerFactory.init(keyStore, "MyPassword".toCharArray());


      // initialize SSLSocketFactory to use the certificates
      SSLSocketFactory socketFactory = null;
      socketFactory = new SSLSocketFactory(SSLSocketFactory.TLS, keyStore, "MyTestPassword2010",
          trustStore, null, null);

      // Set basic data
      HttpParams params = new BasicHttpParams();
      HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
      HttpProtocolParams.setContentCharset(params, "UTF-8");
      HttpProtocolParams.setUseExpectContinue(params, true);
      HttpProtocolParams.setUserAgent(params, "Android app/1.0.0");

      // Make pool
      ConnPerRoute connPerRoute = new ConnPerRouteBean(12);
      ConnManagerParams.setMaxConnectionsPerRoute(params, connPerRoute);
      ConnManagerParams.setMaxTotalConnections(params, 20);

      // Set timeout
      HttpConnectionParams.setStaleCheckingEnabled(params, false);
      HttpConnectionParams.setConnectionTimeout(params, 20 * 1000);
      HttpConnectionParams.setSoTimeout(params, 20 * 1000);
      HttpConnectionParams.setSocketBufferSize(params, 8192);

      // Some client params
      HttpClientParams.setRedirecting(params, false);

      // Register http/s shemas!
      SchemeRegistry schReg = new SchemeRegistry();
      schReg.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
      schReg.register(new Scheme("https", socketFactory, 443));
      ClientConnectionManager conMgr = new ThreadSafeClientConnManager(params, schReg);
      DefaultHttpClient sClient = new DefaultHttpClient(conMgr, params);

      HttpGet httpGet = new HttpGet("https://server/path/service.wsdl");
      HttpResponse response = sClient.execute(httpGet);
      HttpEntity httpEntity = response.getEntity();

      InputStream is = httpEntity.getContent();
      BufferedReader read = new BufferedReader(new InputStreamReader(is));
      String query = null;
      while ((query = read.readLine()) != null)
        System.out.println(query);

    } catch (Exception e) {
      e.printStackTrace();
    }
  }

}
更新:

你也可以直接加载.crt文件到信任库中,而不需要将它们转换为BKS格式:

    private static KeyStore loadTrustStore(String[] certificateFilenames) {
        AssetManager assetsManager = GirdersApp.getInstance().getAssets();

        int length = certificateFilenames.length;
        List<Certificate> certificates = new ArrayList<Certificate>(length);
        for (String certificateFilename : certificateFilenames) {
          InputStream is;
          try {
            is = assetsManager.open(certificateFilename, AssetManager.ACCESS_BUFFER);
            Certificate certificate = KeyStoreManager.loadX509Certificate(is);
            certificates.add(certificate);
          } catch (Exception e) {
            throw new RuntimeException(e);
          }
        }

        Certificate[] certificatesArray = certificates.toArray(new Certificate[certificates.size()]);
          return new generateKeystore(certificatesArray);
      }

 /**
   * Generates keystore congaing the specified certificates.
   *
   * @param certificates certificates to add in keystore
   * @return keystore with the specified certificates
   * @throws KeyStoreException if keystore can not be generated.
   */
  public KeyStore generateKeystore(Certificate[] certificates) throws RuntimeException {
      // construct empty keystore
      KeyStore keyStore = KeyStore.getInstance(keyStoreType);

      // initialize keystore
      keyStore.load(null, null);

      // load certificates into keystore
      int length = certificates.length;
      for (int i = 0; i < length; i++) {
        Certificate certificate = certificates[i];
        keyStore.setEntry(String.valueOf(i), new KeyStore.TrustedCertificateEntry(certificate),
            null);
      }
      return keyStore;
  }

对于带有客户端证书的KeyStore,同样适用,您可以直接使用.p12文件,而无需将其转换为BKS格式。


我确信这段代码可以在 Android 1.6 及以上版本中运行。它被用于我们的旧应用程序中,该应用程序已经在 Android 市场上几年了。也许我修改了一些东西,但是通常情况下它应该可以工作。 - peceps
1
我已经不再参与这个项目,所以我没有亲自确认这个答案,但是我接受它,因为它包含了详细的复制信息,并且根据投票结果看起来是一个可行的解决方案。谢谢。 - Ben Baron
这是一个不太完整的答案:Android也支持其他格式的证书。我们不需要创建BKS密钥库。P12文件也可以直接使用。 - peceps

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接