这篇文章上次修改于 1682 天前,可能其部分内容已经发生变化,如有疑问可询问作者。
本文资料从互联网收集,本人只做了收集整理。文末有引用链接。
0x01 HTTPS 双向认证的握手流程
0x02 单向验证证书
证书锁定
预置的证书通常存储在 Android APP 的 raw
文件夹中/res/raw/customtrustore.bks
,也有时候会存储在/assets/
文件夹下;还有时候硬编码在代码中。
APP代码内置仅接受指定域名的证书,而不接受操作系统或浏览器内置的CA根证书对应的任何证书,通过这种授权方式,保障了APP与服务端通信的唯一性和安全性,因此我们移动端APP与服务端(例如API网关)之间的通信是可以保证绝对安全。但是CA签发证书都存在有效期问题,所以缺点是在证书续期后需要将证书重新内置到APP中。
公钥锁定
预置公钥和预置证书类似,都存储在类似的位置。
公钥锁定则是提取证书中的公钥并内置到移动端APP中,通过与服务器对比公钥值来验证连接的合法性,我们在制作证书密钥时,公钥在证书的续期前后都可以保持不变(即密钥对不变),所以可以避免证书有效期问题。
证书指纹锁定
如果采用证书锁定方式,则获取证书的摘要hash,例如
## 在线读取服务器端.cer格式证书
openssl s_client -connect infinisign.com:443 -showcerts < /dev/null | openssl x509 -outform DER > infinisign.der
## 提取证书的摘要hash并查看base64格式
openssl dgst -sha256 -binary infinisign.der | openssl enc -base64
wLgBEAGmLltnXbK6pzpvPMeOCTKZ0QwrWGem6DkNf6o=
所以其中的wLgBEAGmLltnXbK6pzpvPMeOCTKZ0QwrWGem6DkNf6o=
就是我们将要进行证书锁定的指纹(Hash)信息。
如果采用公钥锁定方式,则获取证书公钥的摘要hash,例如
## 在线读取服务器端证书的公钥
openssl x509 -pubkey -noout -in infinisign.der -inform DER | openssl rsa -outform DER -pubin -in /dev/stdin 2>/dev/null > infinisign.pubkey
## 提取证书的摘要hash并查看base64格式
openssl dgst -sha256 -binary infinisign.pubkey | openssl enc -base64
bAExy9pPp0EnzjAlYn1bsSEGvqYi1shl1OOshfH3XDA=
所以其中的bAExy9pPp0EnzjAlYn1bsSEGvqYi1shl1OOshfH3XDA=
就是我们将要进行证书锁定的指纹(Hash)信息。
证书/公钥锁定例子
常规SSL证书设置
通常由CA权威机构签发的证书,其根证书都内置在最新的Android操作系统中,因此默认情况下可不进行SSL证书锁定,开发APP时也就变得非常简单,以infinisign.com为例,摘自App security best practices
JAVA方法
URL url = new URL("https://infinisign.com");
HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection();
urlConnection.connect();
InputStream in = urlConnection.getInputStream();
KOTLIN方法
val url = URL("https://infinisign.com")
val urlConnection = url.openConnection() as HttpsURLConnection
urlConnection.connect()
urlConnection.inputStream.use {
...
}
网络安全性设置
本方案是官方提供,但需要依赖Android N(Android 7.0 API 24)及以后版本,可在APP开发阶段在APP中内置安全性设置,以达到防止中间人攻击(MITM)的目的,此方法只限制在Android 7.0 API 24以后版本,因此该版本之前的安全性设置仍然需要使用证书锁定方法,本文以infinisign.com为例。
创建配置文件
创建文件res/xml/network_security_config.xml
,需要注意的是,使用证书锁定,需要配置一个备份密钥,假如证书到期或更换了CA品牌后,不至于重新发行APP,这个备份密码可以是中级证书或根证书。
通俗的说,如果系统检测到签发证书过期了,则自动使用其中级或才根级证书作为验证,因为通常中级机构、根机构的证书到期时间非常长。
但实际情况是,infinisign.com所售的CA签发证书有效期都是一年,而现在发行APP或更新APP通常在一年都会有更新重新上架操作。
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config>
<domain includeSubdomains="true">infinisign.com</domain>
<pin digest="SHA-256">wLgBEAGmLltnXbK6pzpvPMeOCTKZ0QwrWGem6DkNf6o</pin>
<!-- 备份密钥,比如infinisign.com的中级机构是geotrust -->
<pin digest="SHA-256">wLgBEAGmLltnXbK6pzpvPMeOCTKZ0QwrWGem6DkNf6o</pin>
</domain-config>
</network-security-config>
引入配置文件
在Androidmanifest.xml
引入配置文件android:networkSecurityConfig="@xml/network_security_config"
<?xml version="1.0" encoding="utf-8"?>
<manifest ... >
<application
android:networkSecurityConfig="@xml/network_security_config">
<!-- application 其它子元素 -->
</application>
</manifest>
OkHttp锁定
OkHttp是一个用于Android处理网络请求的开源项目,是安卓端最流行的轻量级的网络框架,其主要用来替代HttpUrlConnection处理方式
client = new OkHttpClient.Builder()
.certificatePinner(new CertificatePinner.Builder()
.add("infinisign.com", "sha256/S8Ff3JCaO4V...")
.build())
.build();
不过需要注意的是,OkHttp锁定证书方式不适用于公钥锁定方式,必须以证书锁定方式内置SSL数字证书。
TrustManager锁定
TrustManager是一个比较老的证书锁定方法,主要用于早期的Android版本或者用于一些CA根机构在Android系统中缺失根证书的情形下,当然也适用于自签名证书的锁定,通常我们不建议这样做,因为TrustManager的缺点是中间人仍然可以使用成熟的绕过方案来实现截持。
在SSL普及的今天,let's encrypt的开源免费解决方案,和一些入门的便宜DV域名型SSL证书(见PositiveSSL ¥39/年)足以媲美自签名方案。
详细请参考javax.net.ssl.TrustManager接口实现,简单步骤如下
在APP源码中内置证书
/res/raw
使用 KeyStore加载证书
val resourceStream = resources.openRawResource(R.raw.infinisign_cert) val keyStoreType = KeyStore.getDefaultType() val keyStore = KeyStore.getInstance(keyStoreType) keyStore.load(resourceStream, null)
TrustManagerFactory实例化证书
val trustManagerAlgorithm = TrustManagerFactory.getDefaultAlgorithm() val trustManagerFactory = TrustManagerFactory.getInstance(trustManagerAlgorithm) trustManagerFactory.init(keyStore)
创建SSLContext实例,与TrustManager进行绑定。
val sslContext = SSLContext.getInstance("TLS") sslContext.init(null, trustManagerFactory.trustManagers, null) val url = URL("http://infinisign.com/") val urlConnection = url.openConnection() as HttpsURLConnection urlConnection.sslSocketFactory = sslContext.socketFactory
推荐使用:https://github.com/datatheorem/TrustKit-Android
小结
在多数移动操作系统中有大大小小几十个CA机构内置的根证书,但也不排除已经不被信任的CA机构存在的旧根证书,还有一些例如国内的一些基于Android老版本的操作系统仍然面临着安全风险,所以使用SSL数字证书锁定(SSL/TLS Pinning)的目标是缩小可信CA的范围,让APP客户端传送数据更安全,更切实地保障用户数据。
0x03 双向认证证书
预置服务器证书和客户端证书
基于OkHttp + Retrofit + Rxjava实现 HTTPS 双向认证
基于Retrofit实现HTTPS思路
由于Retrofit是基于OkHttp实现的,因此想通过Retrofit实现HTTPS需要给Retrofit设置一个OkHttp代理对象用于处理HTTPS的握手过程。代理代码如下:
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.sslSocketFactory(SSLHelper.getSSLCertifcation(context))//为OkHttp对象设置SocketFactory用于双向认证
.hostnameVerifier(new UnSafeHostnameVerifier())
.build();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://10.2.8.56:8443")
.addConverterFactory(GsonConverterFactory.create())//添加 json 转换器
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())//添加 RxJava 适配器
.client(okHttpClient)//添加OkHttp代理对象
.build();
Android App编写BKS读取创建证书自定义的SSLSocketFactory
private final static String CLIENT_PRI_KEY = "client.bks";
private final static String TRUSTSTORE_PUB_KEY = "truststore.bks";
private final static String CLIENT_BKS_PASSWORD = "123456";
private final static String TRUSTSTORE_BKS_PASSWORD = "123456";
private final static String KEYSTORE_TYPE = "BKS";
private final static String PROTOCOL_TYPE = "TLS";
private final static String CERTIFICATE_FORMAT = "X509";
public static SSLSocketFactory getSSLCertifcation(Context context) {
SSLSocketFactory sslSocketFactory = null;
try {
// 服务器端需要验证的客户端证书,其实就是客户端的keystore
KeyStore keyStore = KeyStore.getInstance(KEYSTORE_TYPE);// 客户端信任的服务器端证书
KeyStore trustStore = KeyStore.getInstance(KEYSTORE_TYPE);//读取证书
InputStream ksIn = context.getAssets().open(CLIENT_PRI_KEY);
InputStream tsIn = context.getAssets().open(TRUSTSTORE_PUB_KEY);//加载证书
keyStore.load(ksIn, CLIENT_BKS_PASSWORD.toCharArray());
trustStore.load(tsIn, TRUSTSTORE_BKS_PASSWORD.toCharArray());
ksIn.close();
tsIn.close();
//初始化SSLContext
SSLContext sslContext = SSLContext.getInstance(PROTOCOL_TYPE);
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(CERTIFICATE_FORMAT);
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(CERTIFICATE_FORMAT);
trustManagerFactory.init(trustStore);
keyManagerFactory.init(keyStore, CLIENT_BKS_PASSWORD.toCharArray());
sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null);
sslSocketFactory = sslContext.getSocketFactory();
} catch (KeyStoreException e) {...}//省略各种异常处理,请自行添加
return sslSocketFactory;
}
Android App获取SSLFactory实例进行网络访问
private void fetchData() {
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.sslSocketFactory(SSLHelper.getSSLCertifcation(context))//获取SSLSocketFactory
.hostnameVerifier(new UnSafeHostnameVerifier())//添加hostName验证器
.build();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://10.2.8.56:8443")//填写自己服务器IP
.addConverterFactory(GsonConverterFactory.create())//添加 json 转换器
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())//添加 RxJava 适配器
.client(okHttpClient)
.build();
IUser userIntf = retrofit.create(IUser.class);
userIntf.getUser(user.getPhone())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Subscriber<UserBean>() {
//省略onCompleted、onError、onNext
}
});
}
private class UnSafeHostnameVerifier implements HostnameVerifier {
@Override
public boolean verify(String hostname, SSLSession session) {
return true;//自行添加判断逻辑,true->Safe,false->unsafe
}
}
基于SslSocketFactory方式双向认证
private static final String KEY_STORE_TYPE_BKS = "bks";//证书类型 固定值
private static final String KEY_STORE_TYPE_P12 = "PKCS12";//证书类型 固定值
private static final String KEY_STORE_CLIENT_PATH = "client.p12";//客户端要给服务器端认证的证书
private static final String KEY_STORE_TRUST_PATH = "client.truststore";//客户端验证服务器端的证书库
private static final String KEY_STORE_PASSWORD = "123456";// 客户端证书密码
private static final String KEY_STORE_TRUST_PASSWORD = "123456";//客户端证书库密码
/**
* 获取SslSocketFactory
*
* @param context 上下文
* @return SSLSocketFactory
*/
public static SSLSocketFactory getSslSocketFactory(Context context) {
try {
// 服务器端需要验证的客户端证书
KeyStore keyStore = KeyStore.getInstance(KEY_STORE_TYPE_P12);
// 客户端信任的服务器端证书
KeyStore trustStore = KeyStore.getInstance(KEY_STORE_TYPE_BKS);
InputStream ksIn = context.getResources().getAssets().open(KEY_STORE_CLIENT_PATH);
InputStream tsIn = context.getResources().getAssets().open(KEY_STORE_TRUST_PATH);
try {
keyStore.load(ksIn, KEY_STORE_PASSWORD.toCharArray());
trustStore.load(tsIn, KEY_STORE_TRUST_PASSWORD.toCharArray());
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
ksIn.close();
} catch (Exception ignore) {
}
try {
tsIn.close();
} catch (Exception ignore) {
}
}
return new SSLSocketFactory(keyStore, KEY_STORE_PASSWORD, trustStore);
} catch (KeyManagementException | UnrecoverableKeyException | KeyStoreException | FileNotFoundException | NoSuchAlgorithmException | ClientProtocolException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
/**
* 获取SSL认证需要的HttpClient
*
* @param context 上下文
* @param port 端口号
* @return HttpClient
*/
public static HttpClient getSslSocketFactoryHttp(Context context, int port) {
HttpClient httpsClient = new DefaultHttpClient();
SSLSocketFactory sslSocketFactory = getSslSocketFactory(context);
if (sslSocketFactory != null) {
Scheme sch = new Scheme("https", sslSocketFactory, port);
httpsClient.getConnectionManager().getSchemeRegistry().register(sch);
}
return httpsClient;
}
基于SSLContext双向认证代码
private static final String KEY_STORE_TYPE_BKS = "bks";//证书类型 固定值
private static final String KEY_STORE_TYPE_P12 = "PKCS12";//证书类型 固定值
private static final String KEY_STORE_CLIENT_PATH = "client.p12";//客户端要给服务器端认证的证书
private static final String KEY_STORE_TRUST_PATH = "client.truststore";//客户端验证服务器端的证书库
private static final String KEY_STORE_PASSWORD = "123456";// 客户端证书密码
private static final String KEY_STORE_TRUST_PASSWORD = "123456";//客户端证书库密码
/**
* 获取SSLContext
*
* @param context 上下文
* @return SSLContext
*/
private static SSLContext getSSLContext(Context context) {
try {
// 服务器端需要验证的客户端证书
KeyStore keyStore = KeyStore.getInstance(KEY_STORE_TYPE_P12);
// 客户端信任的服务器端证书
KeyStore trustStore = KeyStore.getInstance(KEY_STORE_TYPE_BKS);
InputStream ksIn = context.getResources().getAssets().open(KEY_STORE_CLIENT_PATH);
InputStream tsIn = context.getResources().getAssets().open(KEY_STORE_TRUST_PATH);
try {
keyStore.load(ksIn, KEY_STORE_PASSWORD.toCharArray());
trustStore.load(tsIn, KEY_STORE_TRUST_PASSWORD.toCharArray());
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
ksIn.close();
} catch (Exception ignore) {
}
try {
tsIn.close();
} catch (Exception ignore) {
}
}
SSLContext sslContext = SSLContext.getInstance("TLS");
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(trustStore);
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("X509");
keyManagerFactory.init(keyStore, KEY_STORE_PASSWORD.toCharArray());
sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null);
return sslContext;
} catch (Exception e) {
Log.e("tag", e.getMessage(), e);
}
return null;
}
/**
* 获取SSL认证需要的HttpClient
*
* @param context 上下文
* @return OkHttpClient
*/
public static OkHttpClient getSSLContextHttp(Context context) {
OkHttpClient client = new OkHttpClient();
SSLContext sslContext = getSSLContext(context);
if (sslContext != null) {
client.setSslSocketFactory(sslContext.getSocketFactory());
}
return client;
}
/**
* 获取HttpsURLConnection
*
* @param context 上下文
* @param url 连接url
* @param method 请求方式
* @return HttpsURLConnection
*/
public static HttpsURLConnection getHttpsURLConnection(Context context, String url, String method) {
URL u;
HttpsURLConnection connection = null;
try {
SSLContext sslContext = getSSLContext(context);
if (sslContext != null) {
u = new URL(url);
connection = (HttpsURLConnection) u.openConnection();
connection.setRequestMethod(method);//"POST" "GET"
connection.setDoOutput(true);
connection.setDoInput(true);
connection.setUseCaches(false);
connection.setRequestProperty("Content-Type", "binary/octet-stream");
connection.setSSLSocketFactory(sslContext.getSocketFactory());
connection.setConnectTimeout(30000);
}
} catch (Exception e) {
e.printStackTrace();
}
return connection;
}
0x04 证书间转换
使用支持openSSL命令的工具可执行以下命令进行转换
1、DER用于二进制DER编码证书,可以是CER/CRT的表现形式,类似下面的形式
3082 05f7 3082 04df a003 0201 0202 1100
ddd1 cf29 a1bc 4b6d c7ba 2a81 170c 5644
300d 0609 2a86 4886 f70d 0101 0b05 0030
8196 310b 3009 0603 5504 0613 0247 4231
2、CER/CRT等证书格式不含私钥,CER常用于Windows操作系统扩展名,CRT常用于Unix类的系统,例如MacOS或者Linux
x509(cer/crt/pem等)转为pfx格式
如果转换需要设置密码,请输入密码,如不需要则留空
openssl pkcs12 -export -in server.pem -inkey server_private.key -out server.pfx
crt转为pem
openssl x509 -in server.crt -out server.der -outform DER
openssl x509 -in server.der -inform DER -out server.pem -outform PEM
pfx转为pem
openssl pkcs12 -in server.pfx -out server.pem -nodes
pfx提取私钥和公钥(key)
从pfx提取密钥对信息,并转换为key格式(使用pkcs12补全,如果pfx加密,提示输入密码输入即可)
openssl pkcs12 -in server.pfx -nocerts -nodes -out server.key
从密钥对提取私钥
openssl rsa -in server.key -out server_private.key
从密钥对提取公钥
openssl rsa -in server.key -pubout -out server_public.key
如果因为RSA算法使用的是pkcs8模式,需要对提取的私钥使用下述命令
openssl pkcs8 -in server_private.key -out server_private.p8 -outform der -nocrypt -topk8
key转为pvk
pvk是微软专用的用于保存私钥的文件格式
openssl rsa -in server.key -pvk-strong -out server.pvk
x509(cer/crt/pem等)转为spc
spc是微软专用的用于保存公钥的文件格式
openssl crl2pkcs7 -nocrl -certfile server.pem -outform DER -out server.spc
参考
https://www.infinisign.com/faq/what-is-ssl-pinning
https://www.infinisign.com/faq/android-ssl-pinning
https://www.infinisign.com/faq/ssl-tls-transfer
https://developer.android.com/training/articles/security-config
https://www.jianshu.com/p/64172ccfb73b
http://frank-zhu.github.io/android/2014/12/26/android-https-ssl/
http://frank-zhu.github.io/android/2017/03/30/android-https-ssl-part-02/ (Android HTTPS SSL双向验证(CA根证书))
没有评论