Android : 关于HTTPS、TLS/SSL认证以及客户端证书导入方法
一、HTTPS 简介
HTTPS 全称 HTTP over TLS/SSL(TLS就是SSL的新版本3.1)。TLS/SSL是在传输层上层的协议,应用层的下层,作为一个安全层而存在,翻译过来一般叫做传输层安全协议。对 HTTP 而言,安全传输层是透明不可见的,应用层仅仅当做使用普通的 Socket 一样使用 SSLSocket 。TLS是基于 X.509 认证,他假定所有的数字证书都是由一个层次化的数字证书认证机构发出,即 CA。另外值得一提的是 TLS 是独立于 HTTP 的,使用了RSA非对称加密,对称加密以及HASH算法,任何应用层的协议都可以基于 TLS 建立安全的传输通道,如 SSH 协议。
代入场景:假设现在 A 要与远端的 B 建立安全的连接进行通信。
- 直接使用对称加密通信,那么密钥无法安全的送给 B 。
- 直接使用非对称加密,B 使用 A 的公钥加密,A 使用私钥解密。但是因为B无法确保拿到的公钥就是A的公钥,因此也不能防止中间人攻击。
为了解决上述问题,引入了一个第三方,也就是上面所说的 CA(Certificate Authority):
CA 用自己的私钥签发数字证书,数字证书中包含A的公钥。然后 B 可以用 CA 的根证书中的公钥来解密 CA 签发的证书,从而拿到A的公钥。那么又引入了一个问题,如何保证 CA 的公钥是合法的呢?答案就是现代主流的浏览器会内置 CA 的证书。
中间证书:
现在大多数CA不直接签署服务器证书,而是签署中间CA,然后用中间CA来签署服务器证书。这样根证书可以离线存储来确保安全,即使中间证书出了问题,可以用根证书重新签署中间证书。另一个原因是为了支持一些很古老的浏览器,有些根证书本身,也会被另外一个很古老的根证书签名,这样根据浏览器的版本,可能会看到三层或者是四层的证书链结构,如果能看到四层的证书链结构,则说明浏览器的版本很老,只能通过最早的根证书来识别
校验过程
那么实际上,在 HTTPS 握手开始后,服务器会把整个证书链发送到客户端,给客户端做校验。校验的过程是要找到这样一条证书链,链中每个相邻节点,上级的公钥可以校验通过下级的证书,链的根节点是设备信任的锚点或者根节点可以被锚点校验。那么锚点对于浏览器而言就是内置的根证书啦(注:根节点并不一定是根证书)。校验通过后,视情况校验客户端,以及确定加密套件和用非对称密钥来交换对称密钥。从而建立了一条安全的信道。
二、HTTPS API :SSLSocketFactory 或 SSLSocket
Android 使用的是 Java 的 API。那么 HTTPS 使用的 Socket 必然都是通过SSLSocketFactory 创建的 SSLSocket,当然自己实现了 TLS 协议除外。
一个典型的使用 HTTPS 方式如下: (ps:网络连接方式有HttpClient(5.0开始废弃)、HttpURLConnection、OKHttp 和 Volley)
URL url = new URL("https://google.com");
HttpsURLConnection urlConnection = url.openConnection();
InputStream in = urlConnection.getInputStream();
此时使用的是默认的SSLSocketFactory(没有加载自己的证书),与下段代码使用的SSLContext是一致的:
private synchronized SSLSocketFactory getDefaultSSLSocketFactory() {
try {
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, null, null);
return defaultSslSocketFactory = sslContext.getSocketFactory();
} catch (GeneralSecurityException e) {
throw new AssertionError(); // The system has no TLS. Just give up.
}
}
默认的 SSLSocketFactory 校验服务器的证书时,会信任设备内置的100多个根证书。
三、SSL的配置
自定义信任策略
如果不加载自己的证书,系统会为你配置好一个安全的 SSL,但系统默认的 SSL认为一切 CA 都是可信的,可往往 CA 有时候也不可信,比如某家 CA 被黑客入侵什么的事屡见不鲜。虽然 Android 系统自身可以更新信任的 CA 列表,以防止一些 CA 的失效,如果为了更高的安全性,可以希望指定信任的锚点,类似采用如下的代码:
// 取到证书的输入流
InputStream caInput = context.getResources().openRawResource(R.raw.ca_cert);
Certificate ca = CertificateFactory.getInstance("X.509").generateCertificate(caInput); // 创建 Keystore 包含我们的证书
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null, null);
keyStore.setCertificateEntry("ca", ca); // 创建一个 TrustManager 仅把 Keystore 中的证书 作为信任的锚点
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(keyStore); // 用 TrustManager 初始化一个 SSLContext
ssl_ctx = SSLContext.getInstance("TLS"); //定义:public static SSLContext ssl_ctx = null;
ssl_ctx.init(null, tmf.getTrustManagers(), new SecureRandom());
然后可以通过SSLSocketFactory 与服务器进行交互:
// SSLSocketFactory 或 SSLSocket 都行
//1.创建监听指定服务器地址以及指定服务器监听的端口号
SSLSocketFactory socketFactory = (SSLSocketFactory)ssl_ctx.getSocketFactory();
ssl_socket = (SSLSocket) socketFactory.createSocket(serverUrl, Integer.parseInt(serverPort)); //定义:private final String serverUrl = "42.98.106.44";
// private final String serverPort = "8086";
//2.拿到客户端的socket对象的输出/输入流,通过read/write方法和服务器交互数据
ssl_input = new BufferedInputStream(ssl_socket.getInputStream());
ssl_output = new BufferedOutputStream(ssl_socket.getOutputStream());
以上做法只有我们的 ca_cert.crt 才会作为信任的锚点,只有 ca_cert.crt 以及他签发的证书才会被信任。
说起来有个很有趣的玩法,考虑到证书会过期、升级,我们既不想只信任我们服务器的证书,又不想信任 Android 所有的 CA 证书。有个不错的的信任方式是把签发我们服务器的证书的根证书导出打包到 APK 中,然后用上述的方式做信任处理。仔细思考一下,这未尝不是一种好的方式。只要日后换证书还用这家 CA 签发,既不用担心失效,安全性又有了一定的提高。因为比起信任100多个根证书,只信任一个风险会小很多。正如最开始所说,信任锚点未必需要根证书。因此同样上面的代码也可以用于自签名证书的信任,相信看官们能举一反三,就不再多述。
证书固定
上文自定义信任锚点的时候说了一个很有意思的方式,只信任一个根CA,其实更加一般化和灵活的做法就是用证书固定。
其实 HTTPS 是支持证书固定技术的(CertificatePinning),通俗的说就是对证书公钥做校验,看是不是符合期望。HttpsUrlConnection 并没有对外暴露相关的API,而在 Android 大放光彩的 OkHttp 是支持证书固定的,虽然在 Android 中,OkHttp 默认的 SSL 的实现也是调用了 Conscrypt,但是重新用 TrustManager 对下发的证书构建了证书链,并允许用户做证书固定。具体 API 的用法可见 CertificatePinner 这个类,这里不再赘述。
域名校验
Android 内置的 SSL 的实现是引入了Conscrypt 项目,而 HTTP(S)层则是使用的OkHttp。而 SSL 层只负责校验证书的真假,对于所有基于SSL 的应用层协议,需要自己来校验证书实体的身份,因此 Android 默认的域名校验则由 OkHostnameVerifier 实现的,从 HttpsUrlConnection 的代码可见一斑:
static {
try {
defaultHostnameVerifier = (HostnameVerifier)
Class.forName("com.android.okhttp.internal.tls.OkHostnameVerifier")
.getField("INSTANCE").get(null);
} catch (Exception e) {
throw new AssertionError("Failed to obtain okhttp HostnameVerifier", e);
}
}
如果校验规则比较特殊,可以传入自定义的校验规则给 HttpsUrlConnection。同样,如果要基于 SSL 实现其他的应用层协议,千万别忘了做域名校验以证明证书的身份。
四、关于证书
1.证书概念:证书是对现实生活中 某个人或者某件物品的价值体现 比如古董颁发见证书 ,人颁发献血证等 通常证书会包含以下内容:
证书拥有者名称(CN),组织单位(OU)组织(O),城市(L) 区(ST) 国家/地区( C )
证书的过期时间 证书的颁发机构 证书颁发机构对证书的签名,签名算法,对象的公钥等
数字证书的格式遵循X.509标准。X.509是由国际电信联盟(ITU-T)制定的数字证书标准。
2. 证书类型:
P12:是PKCS12的缩写。同样是一个存储私钥的证书库,由.jks文件导出的,用户在PC平台安装,用于标示用户的身份。
CER:俗称数字证书,目的就是用于存储公钥证书,任何人都可以获取这个文件 。
BKS:由于Android平台不识别.keystore和.jks格式的证书库文件,因此Android平台引入一种的证书库格式,BKS。

KeyStore keyStore = KeyStore.getInstance("BKS"); // 访问keytool创建的Java密钥库
InputStream keyStream = context.getResources().openRawResource(R.raw.alitrust); char keyStorePass[]="123456".toCharArray(); //证书密码
keyStore.load(keyStream,keyStorePass); TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(keyStore);//保存服务端的授权证书 ssl_ctx = SSLContext.getInstance("SSL");
ssl_ctx.init(null, trustManagerFactory.getTrustManagers(), null);
3.制作证书:
方式一:利用keytool生成证书
①.生成客户端keystore:
keytool -genkeypair -alias client -keyalg RSA -validity 3650 -keypass 123456 -storepass 123456 -keystore client.jks
②.生成服务端keystore:
keytool -genkeypair -alias server -keyalg RSA -validity 3650 -keypass 123456 -storepass 123456 -keystore server.keystore
//注意:CN必须与IP地址匹配,否则需要修改host
③.导出客户端证书:
keytool -export -alias client -file client.cer -keystore client.jks -storepass 123456
④.导出服务端证书:
keytool -export -alias server -file server.cer -keystore server.keystore -storepass 123456
⑤.证书交换:
将客户端证书导入服务端keystore中,再将服务端证书导入客户端keystore中, 一个keystore可以导入多个证书,生成证书列表。
生成客户端信任证书库(由服务端证书生成的证书库):
keytool -import -v -alias server -file server.cer -keystore truststore.jks -storepass 123456
将客户端证书导入到服务器证书库(使得服务器信任客户端证书):
keytool -import -v -alias client -file client.cer -keystore server.keystore -storepass 123456
⑥.生成Android识别的BKS库文件:
//将client.jks和truststore.jks分别转换成client.bks和truststore.bks,然后放到android客户端的assert目录下,
//然后再通过 Context.getAssets().open("xxx.bks") 获得文件输入流;
keytool -importcert -trustcacerts -keystore key.bks -file client.jks -storetype BKS -provider org.bouncycastle.jce.provider.BouncyCastleProvider
keytool -importcert -trustcacerts -keystore key.bks -file truststore.jks -storetype BKS -provider org.bouncycastle.jce.provider.BouncyCastleProvider
⑦.配置Tomcat服务器:
修改server.xml文件,配置8443端口
<Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol"
maxThreads="150" SSLEnabled="true" scheme="https" secure="true"
clientAuth="true" sslProtocol="TLS"
keystoreFile="${catalina.base}/key/server.keystore" keystorePass="123456"
truststoreFile="${catalina.base}/key/server.keystore" truststorePass="123456"/>
备注: - keystoreFile:指定服务器密钥库,可以配置成绝对路径,本例中是在Tomcat目录中创建了一个名为key的文件夹,仅供参考。
- keystorePass:密钥库生成时的密码
- truststoreFile:受信任密钥库,和密钥库相同即可
- truststorePass:受信任密钥库密码
⑧.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通过OkHttpClient进行网络访问:
//自定义方法,获取OkHttpClient实例:
public static OkHttpClient getOkHttpClient(SSLSocketFactory sslSocketFactory) {
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.connectTimeout(15L, TimeUnit.SECONDS);
builder.sslSocketFactory(sslSocketFactory ); //添加sslSocketFactory
builder.hostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
return true; //自定义判断逻辑:true-安全,false-不安全
}
});
return builder.build();
} ......
//activity端传入之前创建的 sslSocketFactory 拿到 OkHttpClient 实例后便可进行post和get请求:
OkHttpClient okHttpClient = getOkHttpClient(sslSocketFactory);
// 发送格式定义
MediaType JSON
= MediaType.parse("application/json; charset=utf-8");
MediaType STRING
= MediaType.parse("text/x-markdown; charset=utf-8");
// post请求(以json格式发送)=====================================
JSONObject jsonObject = new JSONObject();
jsonObject.put("Model", "KK309");
jsonObject.put("Vid", "0x1234");
jsonObject.put("Pid", "0x5678");
jsonObject.put("Version", 99);
String requestBody = jsonObject.toString(1); final Request postReq = new Request.Builder()
.url(url) //填入自己服务器的URL地址
.post(RequestBody.create(JSON, requestBody))
.build(); Call postCall = okHttpClient.newCall(postReq);
postCall.enqueue(new Callback() { //发送post请求
@Override
public void onFailure(Call call, IOException e) {
Log.d("SSL", "Post ---> onFailure: "+ e);
} @Override
public void onResponse(Call call, Response response) throws IOException {
Log.d("SSL", "Post ---> onResponse: " + response.body().string());
}
}); // get请求===================================================
final Request getReq = new Request.Builder()
.url(url) //填入自己服务器的URL地址
.get() //默认就是GET请求,可以不写
.build(); Call getCall = okHttpClient.newCall(getReq);
getCall.enqueue(new Callback() { //发送get请求
@Override
public void onFailure(Call call, IOException e) {
Log.d("SSL", "Get ---> onFailure: "+ e);
} @Override
public void onResponse(Call call, Response response) throws IOException {
Log.d("SSL", "Get ---> onResponse: " + response.body().string());
}
});
方式二:利用openssl生成证书(keytool没办法签发证书,而openssl能够进行签发和证书链的管理)
①创建CA私钥,创建目录ca:
openssl genrsa -des3 -out ca/ca-key.pem 1024 //-des:表示生成的key是有密码保护的
(注:如果是将生成的key与server的证书一起使用,最好不需要密码,就是不要这个参数,不然客户端每次使用都需要输入密码)
openssl rsa -in ca-key.pem -out ca-key.notneedpassword.pem //也可以用此命令让其不需要输密码
②创建证书请求:
openssl req -new -out ca/ca-req.csr -key ca/ca-key.pem
以下为终端输出信息:
Enter pass phrase for ca/ca-key.pem:
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:CN
State or Province Name (full name) [Some-State]:ZheJiang
Locality Name (eg, city) []:hz
Organization Name (eg, company) [Internet Widgits Pty Ltd]:happylife
Organizational Unit Name (eg, section) []:test
Common Name (e.g. server FQDN or YOUR name) []:test1
Email Address []:test2 Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:123456
An optional company name []:nanosic
③自签署证书:
openssl x509 -req -in ca/ca-req.csr -out ca/ca-cert.pem -signkey ca/ca-key.pem -days 3650
④导出ca证书:
------>生成浏览器支持的.p12格式
openssl pkcs12 -export -clcerts -in ca/ca-cert.pem -inkey ca/ca-key.pem -out ca/ca.p12
只导出ca证书,不导出ca的秘钥:
openssl pkcs12 -export -nokeys -cacerts -in ca/ca-cert.pem -inkey ca/ca-key.pem -out ca/ca1.p12
------>转成Android支持的.BKS格式
keytool -importcert -trustcacerts -keystore key.bks -file ca-cert.pem -storetype BKS -provider org.bouncycastle.jce.provider.BouncyCastleProvider
补充:关于使用keytool生成bks格式证书:
BKS来自BouncyCastleProvider,它使用的也是TripleDES来保护密钥库中的Key,它能够防止证书库被不小心修改(Keystore的keyentry改掉1个bit都会产生错误),BKS能够跟JKS互操作;

②.放到本机JDK的安装目录\jre\lib\ext 下面,然后便可通过前面的方法使用keytool生成BSK证书。
-end-
Android : 关于HTTPS、TLS/SSL认证以及客户端证书导入方法的更多相关文章
- php实现https(tls/ssl)双向认证
php实现https(tls/ssl)双向认证 通常情况下,在部署https的时候,是基于ssl单向认证的,也就是说只要客户端认证服务器,而服务器不需要认证客户端. 但在一些安全性较高的场景,如银行, ...
- Wamp Https 的 SSL认证 配置说明
Wamp Https 的 SSL认证 配置说明版本 Apache2.2.11注:右下角图标的 重启 不能有效加载 配置文件 应退出后重新运行注:C:\wamp\bin\apache\Apache2.2 ...
- 你不可不知的WEB安全知识(第一部分:HTTPS, TLS, SSL, CORS, CSP)
译 原文地址:https://dev.to/ahmedatefae/web-security-knowledge-you-must-understand-it-part-i-https-tls-s ...
- AFNetWorking https请求 SSL认证 自制证书
1.服务器会给一个证书,一般为.pem格式证书 2.将.pem格式的证书转换成.cer格式的证书 打开电脑自带终端 ,进入到桌面 cd Desktop 回车回到桌面Desktop Admin$ 输入 ...
- Tomcat 实现双向SSL认证
大概思路: 使用openssl生产CA证书,使用keytool生产密钥库 实验环境:RHEL6.4+Tomcat8 一.生成CA根证书,并自签名 1.生成CA密钥 # genrsa [产生密钥命令] ...
- https 单向双向认证说明_数字证书, 数字签名, SSL(TLS) , SASL_转
转自:https 单向双向认证说明_数字证书, 数字签名, SSL(TLS) , SASL 因为项目中要用到TLS + SASL 来做安全认证层. 所以看了一些网上的资料, 这里做一个总结. 1. 首 ...
- 转: https 单向双向认证说明_数字证书, 数字签名, SSL(TLS) , SASL
转自: http://www.cnblogs.com/mailingfeng/archive/2012/07/18/2597392.html 因为项目中要用到TLS + SASL 来做安全认证层. 所 ...
- Https系列之四:https的SSL证书在Android端基于okhttp,Retrofit的使用
Https系列会在下面几篇文章中分别作介绍: 一:https的简单介绍及SSL证书的生成二:https的SSL证书在服务器端的部署,基于tomcat,spring boot三:让服务器同时支持http ...
- 基于mosquitto的MQTT服务器---SSL/TLS 单向认证+双向认证
基于mosquitto的MQTT服务器---SSL/TLS 单向认证+双向认证 摘自:https://blog.csdn.net/ty1121466568/article/details/811184 ...
随机推荐
- java8模拟grouby方法
public class ListUtils{ /** * list 集合分组 * * @param list 待分组集合 * @param groupBy 分组Key算法 * @param < ...
- 擦他丫的,今天在Django项目中引用静态文件jQuery.js 就是引入报错,终于找到原因了!
擦 ,今天在Django项目中引用静态文件jQuery.js 就是引入报错,终于找到原因了! 问题在于我使用的谷歌浏览器,默认使用了缓存,导致每次访问同一个url时,都返回的是缓存里面的东西.通过谷歌 ...
- sql server 按年月日分组
sql server 按年月日分组 ----------------------------------------------- --author:yangjinwang --date:2017- ...
- [ Build Tools ] Repositories
仓库介绍 http://hao.jobbole.com/central-repository/ https://my.oschina.net/pingjiangyetan/blog/423380 ht ...
- 221. Maximal Square(动态规划)
Given a 2D binary matrix filled with 0's and 1's, find the largest square containing only 1's and re ...
- Django框架详细介绍---ORM---图书信息系统专题训练
from django.db import models # Create your models here. # 书 class Book(models.Model): title = models ...
- Java 多重catch语句的具体使用介绍
某些情况,由单个代码段可能引起多个异常.处理这种情况,你可以定义两个或更多的catch子句,每个子句捕获一种类型的异常.当异常被引发时,每一个catch子句被依次检查,第一个匹配异常类型的子句执行.当 ...
- 2019/3/28 wen 继承
- 后端解决 微信H5支付 商户参数格式错误 方法
问题如图: 后端解决方法: 在返回mweb_url 后不要直接访问这个链接,在当前页面用js window.location.href = mweb_url 这样跳转就可以了
- openwrt修改hosts
不同于标准linux主机,openwrt使用dnsmasq来管理dns和dhcp. 修改dnsmasq的配置文件 vi /etc/config/dhcp 在config dnsmasq这组下面添加 l ...