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 ...
随机推荐
- .Net Core创建Docker镜像
1..Net Core项目[Lails.Server.Demo]发布到目录下Lails.Server.Demo\bin\Release\netcoreapp2.1\publish 2.上面目录下新建文 ...
- 使用rander() 将后台的数据传递到前台界面显示出来
1.创建templates文件夹 2.在该文件夹内创建html界面a.html 3.views.py: def a(request): love='iloveyou' return render(re ...
- Python之模块导入
import sys #import module (.py)import functools #名词空间 functoolsprint(functools) print("-------- ...
- 给学习立个flag
今天是2018年7月7号,此时的砖相比昨天格外烫手,望着手套因被磨破而露出来的半截手指头,一股股热浪溜溜的从指间划过,背后还有小山一样高的砖头,感觉对面today店里的冰镇西瓜又成了不可奢望的梦... ...
- React 最简单的入门教程
一看就懂的ReactJs入门教程(精华版) 现在最热门的前端框架有AngularJS.React.Bootstrap等.自从接触了ReactJS,ReactJs的虚拟DOM(Virtual D ...
- TCP/IP协议详解内容总结(怒喷一口老血)
TCP/IP协议(本文源自外部链接) TCP/IP不是一个协议,而是一个协议族的统称.里面包括IP协议.IMCP协议.TCP协议. 这里有几个需要注意的知识点: 互联网地址:也就是IP地址,一般为网络 ...
- for循环介绍
流程控制之for循环names=['yb','zs','yxd','lb'] i=0 while i < len(names): #4 < 4 print(names[i]) i+=1 # ...
- Win32汇编学习(5):绘制文本2
这次我们将学习有关文本的诸多属性如字体和颜色等. 理论: Windows 的颜色系统是用RGB值来表示的,R 代表红色,G 代表绿色,B 代表蓝色.如果您想指定一种颜色就必须给该颜色赋相关的 RGB ...
- centos6.5下安装Redis
已有redis-3.2.1.tar.gz文件 拖到centos系统的桌面 现在在桌面目录下 tar -zxv -f redis-3.2.1.tar.gz以解压压缩包 cd redis-3.2.1以切换 ...
- C# ToLookup
下文参考翻译自: C#/.NET Little Wonders: The ToLookup() LINQ Extension Method 故事的背景 让我们先来创建一个简单的类来表示产品,产品有ID ...