与 Android 应用程序中的客户端证书的 HTTPS 连接
Posted
技术标签:
【中文标题】与 Android 应用程序中的客户端证书的 HTTPS 连接【英文标题】:HTTPS connection with client certificate in an android app 【发布时间】:2011-12-04 15:01:12 【问题描述】:我正在尝试用我正在编写的 android 应用程序中的 HTTPS 连接替换当前工作的 HTTP 连接。 HTTPS 连接的额外安全性是必要的,所以我不能忽略这一步。
我有以下几点:
-
配置为建立 HTTPS 连接并需要客户端证书的服务器
此服务器具有由标准大型 CA 颁发的证书。简而言之,如果我通过 Android 中的浏览器访问此连接,它可以正常工作,因为设备信任库可以识别 CA。 (所以它不是自签名的)
本质上是自签名的客户端证书。 (由内部 CA 颁发)
加载此客户端证书并尝试连接到上述服务器的 Android 应用程序,但存在以下问题/属性:
当服务器配置为不需要客户端证书时,客户端可以连接到服务器。基本上,如果我使用
SSLSocketFactory.getSocketFactory()
连接工作正常,但客户端证书是此应用程序规范的必需部分,所以:
当我尝试连接我的自定义SSLSocketFactory
时,客户端会产生javax.net.ssl.SSLPeerUnverifiedException: No peer certificate
异常,但我不完全确定原因。在互联网上搜索了各种解决方案后,这个例外似乎有点模棱两可。
这是客户端的相关代码:
SSLSocketFactory socketFactory = null;
public void onCreate(Bundle savedInstanceState)
loadCertificateData();
private void loadCertificateData()
try
File[] pfxFiles = Environment.getExternalStorageDirectory().listFiles(new FileFilter()
public boolean accept(File file)
if (file.getName().toLowerCase().endsWith("pfx"))
return true;
return false;
);
InputStream certificateStream = null;
if (pfxFiles.length==1)
certificateStream = new FileInputStream(pfxFiles[0]);
KeyStore keyStore = KeyStore.getInstance("PKCS12");
char[] password = "somePassword".toCharArray();
keyStore.load(certificateStream, password);
System.out.println("I have loaded [" + keyStore.size() + "] certificates");
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, password);
socketFactory = new SSLSocketFactory(keyStore);
catch (Exceptions e)
// Actually a bunch of catch blocks here, but shortened!
private void someMethodInvokedToEstablishAHttpsConnection()
try
HttpParams standardParams = new BasicHttpParams();
HttpConnectionParams.setConnectionTimeout(standardParams, 5000);
HttpConnectionParams.setSoTimeout(standardParams, 30000);
SchemeRegistry schRegistry = new SchemeRegistry();
schRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
schRegistry.register(new Scheme("https", socketFactory, 443));
ClientConnectionManager connectionManager = new ThreadSafeClientConnManager(standardParams, schRegistry);
HttpClient client = new DefaultHttpClient(connectionManager, standardParams);
HttpPost request = new HttpPost();
request.setURI(new URI("https://TheUrlOfTheServerIWantToConnectTo));
request.setEntity("Some set of data used by the server serialized into string format");
HttpResponse response = client.execute(request);
resultData = EntityUtils.toString(response.getEntity());
catch (Exception e)
// Catch some exceptions (Actually multiple catch blocks, shortened)
我已经验证了,是的,keyStore 确实加载了一个证书,并且对此很满意。
对于阅读有关 HTTPS/SSL 连接的内容,我有两种理论,但由于这确实是我的第一次尝试,我对解决这个问题真正需要什么感到有点困惑。
据我所知,第一种可能性是我需要使用设备的信任库配置此 SSLSocketFactory,其中包括所有标准的中间和端点证书颁发机构。也就是说,设备的默认值SSLSocketFactory.getSocketFactory()
将一组 CA 加载到工厂的信任库中,用于在服务器发送证书时信任服务器,这就是我的代码失败的原因,因为我没有正确地信任商店加载。如果这是真的,我最好如何加载这些数据?
第二种可能性是由于客户端证书是自签名的(或由内部证书颁发机构颁发的 - 如果我错了,请纠正我,但这些实际上等同于所有意图和这里的目的)。实际上,我缺少的是 this 信任库,基本上我需要为服务器提供一种方法来验证内部 CA 的证书,并验证这个内部 CA 实际上是 “值得信赖”。如果这是真的,我到底在寻找什么样的东西?我看到了一些对此的参考,这让我相信这可能是我的问题,如here,但我真的不确定。如果这确实是我的问题,我会向维护内部 CA 的人提出什么要求,然后我将如何将其添加到我的代码中以便我的 HTTPS 连接能够正常工作?
第三个,希望不太可能的解决方案是,我在这里的某些观点完全错误,错过了关键步骤,或者完全忽略了我目前不了解的 HTTPS/SSL 的一部分.如果是这种情况,请您给我一个方向,以便我可以去了解我需要学习的内容吗?
感谢阅读!
【问题讨论】:
您的服务器还需要验证您提到的客户端证书。如果您熟悉 WireShark,您可以检查 TLS 握手以了解服务器如何响应客户端证书 @jglouie 我不熟悉 WireShark,但听起来我应该熟悉。我去看看! 【参考方案1】:有一种更简单的方法可以实现@jglouie 的解决方案。
基本上,如果您使用SSLContext
并使用null
作为信任管理器参数对其进行初始化,那么您应该使用默认信任管理器获取 SSL 上下文。请注意,这没有在 Android 文档中记录,但 Java documentation for SSLContext.init 说
前两个参数中的任何一个都可以为空,在这种情况下,将搜索已安装的安全提供程序以查找相应工厂的最高优先级实现。
代码如下所示:
// This can be any protocol supported by your target devices.
// For example "TLSv1.2" is supported by the latest versions of Android
final String SSL_PROTOCOL = "TLS";
try
sslContext = SSLContext.getInstance(SSL_PROTOCOL);
// Initialize the context with your key manager and the default trust manager
// and randomness source
sslContext.init(keyManagerFactory.getKeyManagers(), null, null);
catch (NoSuchAlgorithmException e)
Log.e(TAG, "Specified SSL protocol not supported! Protocol=" + SSL_PROTOCOL);
e.printStackTrace();
catch (KeyManagementException e)
Log.e(TAG, "Error setting up the SSL context!");
e.printStackTrace();
// Get the socket factory
socketFactory = sslContext.getSocketFactory();
【讨论】:
我记得两年前我在寻找解决方案时读过这篇文章。我很确定在 Android 的特殊风格下它在传递 null 时无法正常工作,但我同意它应该具有并且它是一个更好的解决方案。【参考方案2】:我认为这确实是问题所在。
据我所知,第一种可能性是我需要 使用设备的信任库配置此 SSLSocketFactory 包括所有标准的中间证书和端点证书 当局
如果这是真的,我最好如何加载这些数据?
尝试这样的事情(你需要让你的套接字工厂使用这个默认的信任管理器):
X509TrustManager manager = null;
FileInputStream fs = null;
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
try
fs = new FileInputStream(System.getProperty("javax.net.ssl.trustStore"));
keyStore.load(fs, null);
finally
if (fs != null) fs.close();
trustManagerFactory.init(keyStore);
TrustManager[] managers = trustManagerFactory.getTrustManagers();
for (TrustManager tm : managers)
if (tm instanceof X509TrustManager)
manager = (X509TrustManager) tm;
break;
编辑:在使用此处的代码之前,请先查看 Pooks 的答案。听起来现在有更好的方法可以做到这一点。
【讨论】:
我没有添加socketFactory = new SSLSocketFactory(keyStore);
,而是添加了您的代码(尽管我将您的KeyStore keyStore
重命名为KeyStore trustStore
)并在loadCertificateData()
函数的末尾使用了以下代码:socketFactory = new SSLSocketFactory(keyStore, new String(password), trustStore);
--效果很好,谢谢!
我不需要 foreach 循环来获取 TrustManager。为什么你认为这是必要的? (我想我通过将整个信任库交给 SSLSocketFactory 来规避这个问题?)
@Kevek iam 还尝试了基于证书的身份验证。但我得到了“javax.net.ssl.SSLHandshakeException:java.security.cert.CertPathValidatorException:找不到证书路径的信任锚。”例外..帮我解决它
***.com/questions/12468526/… 似乎表明您没有找到信任库。是否有可能在您使用的任何类型的 Android 上,该位置都没有存储在 javax.net.ssl.trustStore 中?您在什么操作中收到此错误?
你好@Kevek 我需要实现你实现的同样的东西,我将客户端的证书和私钥加载到 KeyStore 并作为 SSLContext.init 函数的第一个参数传递,然后我将 SocketFactor 创建为sslContext.getSocketFactory()
并将其设置为client2.setSslSocketFactory(sslContext.getSocketFactory());
并且证书不会传递给服务器。这是因为我使用的是 getSocketFactory() 吗?你能分享你的 SSLSocketFactory 课程吗?【参考方案3】:
我试了几天 我终于得到答案了 所以我想在这里发布我的步骤和所有代码以帮助其他人。
1) 获取要连接站点的证书
echo | openssl s_client -connect $MY_SERVER:443 2>&1 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > mycert.pem
2) 要创建您的密钥,您需要 BouncyCastle 库,您可以下载 here
keytool -import -v -trustcacerts -alias 0 -file mycert.pem -keystore “store_directory/mykst“ -storetype BKS -provider org.bouncycastle.jce.provider.BouncyCastleProvider -providerpath “directory_of_bouncycastle/bcprov-jdk16-145.jar” -storepass mypassword
3) 检查是否创建了密钥
keytool -list -keystore "carpeta_almacen/mykst" -provider org.bouncycastle.jce.provider.BouncyCastleProvider -providerpath "directory_of_bouncycastle/bcprov-jdk16-145.jar" -storetype BKS -storepass mypassword
你应该看到这样的东西:
Tipo de almacén de claves:BKS Proveedor de almacén de claves: BC
Su almacén de claves contiene entrada 1
0, 07-dic-2011,trustedCertEntry,
Huella 数字证书 (MD5):
55:FD:E5:E3:8A:4C:D6:B8:69:EB:6A:49:05:5F:18:48
4)然后你需要将文件“mykst”复制到你的android项目中的目录“res/raw”中(如果不存在则创建它)。
5)在android清单中添加权限
<uses-permission android:name="android.permission.INTERNET"/>
6) 这里是代码!
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_
android:layout_
android:orientation="vertical"
android:padding="10dp" >
<Button
android:id="@+id/button"
android:layout_
android:layout_
android:text="Cargar contenido" />
<RelativeLayout
android:layout_
android:layout_
android:background="#4888ef">
<ProgressBar
android:id="@+id/loading"
android:layout_
android:layout_
android:indeterminate="true"
android:layout_centerInParent="true"
android:visibility="gone"/>
<ScrollView
android:layout_
android:layout_
android:fillViewport="true"
android:padding="10dp">
<TextView
android:id="@+id/output"
android:layout_
android:layout_
android:textColor="#FFFFFF"/>
</ScrollView>
</RelativeLayout>
</LinearLayout>
MyHttpClient
package com.example.https;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.KeyStore;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.Enumeration;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.SingleClientConnManager;
import android.content.Context;
import android.os.Build;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.TrustManagerFactory;
public class MyHttpClient extends DefaultHttpClient
final Context context;
public MyHttpClient(Context context)
this.context = context;
@Override
protected ClientConnectionManager createClientConnectionManager()
SchemeRegistry registry = new SchemeRegistry();
registry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
// Register for port 443 our SSLSocketFactory with our keystore
// to the ConnectionManager
registry.register(new Scheme("https", newSslSocketFactory(), 443));
return new SingleClientConnManager(getParams(), registry);
private SSLSocketFactory newSslSocketFactory()
try
// Trust manager / truststore
KeyStore trustStore=KeyStore.getInstance(KeyStore.getDefaultType());
// If we're on an OS version prior to Ice Cream Sandwich (4.0) then use the standard way to get the system
// trustStore -- System.getProperty() else we need to use the special name to get the trustStore KeyStore
// instance as they changed their trustStore implementation.
if (Build.VERSION.RELEASE.compareTo("4.0") < 0)
TrustManagerFactory trustManagerFactory=TrustManagerFactory
.getInstance(TrustManagerFactory.getDefaultAlgorithm());
FileInputStream trustStoreStream=new FileInputStream(System.getProperty("javax.net.ssl.trustStore"));
trustStore.load(trustStoreStream, null);
trustManagerFactory.init(trustStore);
trustStoreStream.close();
else
trustStore=KeyStore.getInstance("AndroidCAStore");
InputStream certificateStream = context.getResources().openRawResource(R.raw.mykst);
KeyStore keyStore=KeyStore.getInstance("BKS");
try
keyStore.load(certificateStream, "mypassword".toCharArray());
Enumeration<String> aliases=keyStore.aliases();
while (aliases.hasMoreElements())
String alias=aliases.nextElement();
if (keyStore.getCertificate(alias).getType().equals("X.509"))
X509Certificate cert=(X509Certificate)keyStore.getCertificate(alias);
if (new Date().after(cert.getNotAfter()))
// This certificate has expired
return null;
catch (IOException ioe)
// This occurs when there is an incorrect password for the certificate
return null;
finally
certificateStream.close();
KeyManagerFactory keyManagerFactory=KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, "mypassword".toCharArray());
return new SSLSocketFactory(keyStore, "mypassword", trustStore);
catch (Exception e)
throw new AssertionError(e);
MainActivity
package com.example.https;
import android.app.Activity;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringWriter;
import java.io.Writer;
import javax.net.ssl.SSLSocketFactory;
public class MainActivity extends Activity
private View loading;
private TextView output;
private Button button;
SSLSocketFactory socketFactory = null;
@Override
public void onCreate(Bundle savedInstanceState)
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
loading = findViewById(R.id.loading);
output = (TextView) findViewById(R.id.output);
button = (Button) findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener()
@Override
public void onClick(View v)
new CargaAsyncTask().execute(new Void[0]);
);
class CargaAsyncTask extends AsyncTask<Void, Void, String>
@Override
protected void onPreExecute()
super.onPreExecute();
loading.setVisibility(View.VISIBLE);
button.setEnabled(false);
@Override
protected String doInBackground(Void... params)
// Instantiate the custom HttpClient
DefaultHttpClient client = new MyHttpClient(getApplicationContext());
HttpGet get = new HttpGet("https://www.google.com");
// Execute the GET call and obtain the response
HttpResponse getResponse;
String resultado = null;
try
getResponse = client.execute(get);
HttpEntity responseEntity = getResponse.getEntity();
InputStream is = responseEntity.getContent();
resultado = convertStreamToString(is);
catch (ClientProtocolException e)
e.printStackTrace();
catch (IOException e)
e.printStackTrace();
return resultado;
@Override
protected void onPostExecute(String result)
super.onPostExecute(result);
loading.setVisibility(View.GONE);
button.setEnabled(true);
if (result == null)
output.setText("Error");
else
output.setText(result);
public static String convertStreamToString(InputStream is) throws IOException
/*
* To convert the InputStream to String we use the
* Reader.read(char[] buffer) method. We iterate until the
* Reader return -1 which means there's no more data to
* read. We use the StringWriter class to produce the string.
*/
if (is != null)
Writer writer = new StringWriter();
char[] buffer = new char[1024];
try
Reader reader = new BufferedReader(new InputStreamReader(is, "UTF-8"));
int n;
while ((n = reader.read(buffer)) != -1)
writer.write(buffer, 0, n);
finally
is.close();
return writer.toString();
else
return "";
我希望它对其他人有用! 尽情享受吧!
【讨论】:
为了使用自签名证书。首先,您必须创建自己的 CA (g-loaded.eu/2005/11/10/be-your-own-ca),然后签署证书并将其使用到您的服务器中,然后将 CA 安装到您的手机 (jethrocarr.com/2012/01/04/custom-ca-certificates-and-android) 中,它应该可以工作【参考方案4】:看来您还需要为 SSLSocketFactory 设置主机名。
尝试添加行
socketFactory.setHostnameVerifier(SSLSocketFactory.STRICT_HOSTNAME_VERIFIER);
在与您的SSLFactory
建立新连接之前。
除了结构上的不同,我们还有类似的代码。在我的实现中,我刚刚创建了自己的 DefaultHttpClient 扩展,它看起来类似于上面的大部分代码。如果这不能解决问题,我可以为此发布工作代码,您可以尝试这种方法。
编辑:这是我的工作版本
public class ActivateHttpClient extends DefaultHttpClient
final Context context;
/**
* Public constructor taking two arguments for ActivateHttpClient.
* @param context - Context referencing the calling Activity, for creation of
* the socket factory.
* @param params - HttpParams passed to this, specifically to set timeouts on the
* connection.
*/
public ActivateHttpClient(Context context, HttpParams params)
this.setParams(params);
/* (non-Javadoc)
* @see org.apache.http.impl.client.DefaultHttpClient#createClientConnectionManager()
* Create references for both http and https schemes, allowing us to attach our custom
* SSLSocketFactory to either
*/
@Override
protected ClientConnectionManager createClientConnectionManager()
SchemeRegistry registry = new SchemeRegistry();
registry.register(new Scheme("http", PlainSocketFactory
.getSocketFactory(), 80));
registry.register(new Scheme("https", newSslSocketFactory(), 443));
return new SingleClientConnManager(getParams(), registry);
/**
* Creation of new SSLSocketFactory, which imports a certificate from
* a server which self-signs its own certificate.
* @return
*/
protected SSLSocketFactory newSslSocketFactory()
try
//Keystore must be in BKS (Bouncy Castle Keystore)
KeyStore trusted = KeyStore.getInstance("BKS");
//Reference to the Keystore
InputStream in = context.getResources().openRawResource(
R.raw.cert);
//Password to the keystore
try
trusted.load(in, PASSWORD_HERE.toCharArray());
finally
in.close();
// Pass the keystore to the SSLSocketFactory. The factory is
// responsible
// for the verification of the server certificate.
SSLSocketFactory sf = new SSLSocketFactory(trusted);
// Hostname verification from certificate
// http://hc.apache.org/httpcomponents-client-ga/tutorial/html/connmgmt.html#d4e506
sf.setHostnameVerifier(SSLSocketFactory.STRICT_HOSTNAME_VERIFIER);
return sf;
// return new SSLSocketFactory(trusted);
catch (Exception e)
e.printStackTrace();
throw new AssertionError(e);
并且可以如图所示调用:
HttpParams params = new BasicHttpParams();
// Set the timeout in milliseconds until a connection is established.
int timeoutConnection = 500;
HttpConnectionParams.setConnectionTimeout( params , timeoutConnection );
// Set the default socket timeout (SO_TIMEOUT)
// in milliseconds which is the timeout for waiting for data.
int timeoutSocket = 1000;
HttpConnectionParams.setSoTimeout( params , timeoutSocket );
//ADD more connection options here!
String url =
"https:// URL STRING HERE";
HttpGet get = new HttpGet( url );
ActivateHttpClient client =
new ActivateHttpClient( this.context, params );
// Try to execute the HttpGet, throwing errors
// if no response is received, or if there is
// an error in the execution.
HTTPResponse response = client.execute( get );
【讨论】:
我尝试了这个,在我的代码中添加了您在创建 SSLSocketFactory 后提到的行:socketFactory = new SSLSocketFactory(keyStore);
。然而,这似乎并没有改变任何东西。当我尝试调用 client.execute(request)
时,我仍然收到“SSLPeerUnverifiedException: No Peer Certificate”错误【参考方案5】:
我发布了一个更新的答案,因为人们仍然参考这个问题并对此问题进行投票。我不得不多次更改套接字工厂代码,因为自 Android 4.0 以来有些事情发生了变化
// Trust manager / truststore
KeyStore trustStore=KeyStore.getInstance(KeyStore.getDefaultType());
// If we're on an OS version prior to Ice Cream Sandwich (4.0) then use the standard way to get the system
// trustStore -- System.getProperty() else we need to use the special name to get the trustStore KeyStore
// instance as they changed their trustStore implementation.
if (Build.VERSION.RELEASE.compareTo("4.0") < 0)
TrustManagerFactory trustManagerFactory=TrustManagerFactory
.getInstance(TrustManagerFactory.getDefaultAlgorithm());
FileInputStream trustStoreStream=new FileInputStream(System.getProperty("javax.net.ssl.trustStore"));
trustStore.load(trustStoreStream, null);
trustManagerFactory.init(trustStore);
trustStoreStream.close();
else
trustStore=KeyStore.getInstance("AndroidCAStore");
InputStream certificateStream=new FileInputStream(userCertFile);
KeyStore keyStore=KeyStore.getInstance("PKCS12");
try
keyStore.load(certificateStream, certPass.toCharArray());
Enumeration<String> aliases=keyStore.aliases();
while (aliases.hasMoreElements())
String alias=aliases.nextElement();
if (keyStore.getCertificate(alias).getType().equals("X.509"))
X509Certificate cert=(X509Certificate)keyStore.getCertificate(alias);
if (new Date().after(cert.getNotAfter()))
// This certificate has expired
return;
catch (IOException ioe)
// This occurs when there is an incorrect password for the certificate
return;
finally
certificateStream.close();
KeyManagerFactory keyManagerFactory=KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, certPass.toCharArray());
socketFactory=new SSLSocketFactory(keyStore, certPass, trustStore);
希望这对将来仍然来这里的人有所帮助。
【讨论】:
感谢您的信息,但我认为这是很长的路要走,因为我在没有 trustKeyStore 的情况下通过调用SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(kmf.getKeyManagers(), null, null);client.setSslSocketFactory(sslContext.getSocketFactory());
(我的问题是我加载了错误的私钥到密钥库。)以上是关于与 Android 应用程序中的客户端证书的 HTTPS 连接的主要内容,如果未能解决你的问题,请参考以下文章
什么是 android 中 paytm 支付网关集成中的客户端证书?