Android : 关于HTTPS、TLS/SSL认证以及客户端证书导入方法

1、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 协议。html

 

  代入场景:假设如今 A 要与远端的 B 创建安全的链接进行通讯。

  1. 直接使用对称加密通讯,那么密钥没法安全的送给 B 。
  2. 直接使用非对称加密,B 使用 A 的公钥加密,A 使用私钥解密。可是由于B没法确保拿到的公钥就是A的公钥,所以也不能防止中间人攻击。

     为了解决上述问题,引入了一个第三方,也就是上面所说的 CA(Certificate Authority):  android

    CA 用本身的私钥签发数字证书,数字证书中包含A的公钥。而后 B 能够用 CA 的根证书中的公钥来解密 CA 签发的证书,从而拿到A的公钥。那么又引入了一个问题,如何保证 CA 的公钥是合法的呢?答案就是现代主流的浏览器会内置 CA 的证书。git

  中间证书:

    如今大多数CA不直接签署服务器证书,而是签署中间CA,而后用中间CA来签署服务器证书。这样根证书能够离线存储来确保安全,即便中间证书出了问题,能够用根证书从新签署中间证书。另外一个缘由是为了支持一些很古老的浏览器,有些根证书自己,也会被另一个很古老的根证书签名,这样根据浏览器的版本,可能会看到三层或者是四层的证书链结构,若是能看到四层的证书链结构,则说明浏览器的版本很老,只能经过最先的根证书来识别算法

  校验过程

    那么实际上,在 HTTPS 握手开始后,服务器会把整个证书链发送到客户端,给客户端作校验。校验的过程是要找到这样一条证书链,链中每一个相邻节点,上级的公钥能够校验经过下级的证书,链的根节点是设备信任的锚点或者根节点能够被锚点校验。那么锚点对于浏览器而言就是内置的根证书啦(注:根节点并不必定是根证书)。校验经过后,视状况校验客户端,以及肯定加密套件和用非对称密钥来交换对称密钥。从而创建了一条安全的信道。apache

 

2、HTTPS API :SSLSocketFactory SSLSocket

  Android 使用的是 Java 的 API。那么 HTTPS 使用的 Socket 必然都是经过SSLSocketFactory 建立的 SSLSocket,固然本身实现了 TLS 协议除外。json

一个典型的使用 HTTPS 方式以下: (ps:网络链接方式有HttpClient(5.0开始废弃)、HttpURLConnection、OKHttp 和 Volley)api

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多个根证书。安全

 

3、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 实现其余的应用层协议,千万别忘了作域名校验以证实证书的身份。

 

4、关于证书

 1.证书概念:证书是对现实生活中 某我的或者某件物品的价值体现 好比古董颁发见证书 ,人颁发献血证等 一般证书会包含如下内容:

          证书拥有者名称(CN),组织单位(OU)组织(O),城市(L) 区(ST) 国家/地区( C )

               证书的过时时间 证书的颁发机构 证书颁发机构对证书的签名,签名算法,对象的公钥等

               数字证书的格式遵循X.509标准。X.509是由国际电信联盟(ITU-T)制定的数字证书标准。

  

 2. 证书类型:

JKS:数字证书库。 JKS里有KeyEntry和CertEntry,在库里的每一个Entry都是靠别名(alias)来识别的。
P12:是PKCS12的缩写。一样是一个 存储私钥的证书库,由 .jks文件导出的,用户在PC平台安装, 用于标示用户的身份
CER:俗称数字证书, 目的就是用于存储公钥证书,任何人均可以获取这个文件 。
BKS:因为Android平台不识别 .keystore.jks格式的证书库文件,所以Android平台引入一种的证书库格式,BKS。
 
下图展现了证书的使用流程:
 
为何Tomcat只有一个server.keystore文件,而客户端须要两个库文件?
  由于有时客户端可能须要访问多个服务器,而服务器的证书都不相同,所以客户端须要制做一个 truststore来存储受信任的服务器的证书列表。所以为了规范建立一个 truststore.jks用于存储全部受信任的服务器证书,建立一个 client.jks来存储客户端本身的私钥。对于只涉及与一个服务端进行双向认证的应用,将 server.cer导入到 client.jks中便可。
 
导入BKS使用代码示例:(上面“ SSL的配置”部分已展现过导入证书的方式)
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格式证书

    JKS和JCEKS是Java密钥库(KeyStore)的两种比较常见类型,JKS的Provider是SUN,在每一个版本的JDK中都有;
    BKS来自BouncyCastleProvider,它使用的也是TripleDES来保护密钥库中的Key,它可以防止证书库被不当心修改(Keystore的keyentry改掉1个bit都会产生错误),BKS可以跟JKS互操做;
    而jdk的keytool只能生成jks的证书库,若是生成bks的则须要下载BouncyCastle库,参考以下配置环境:
    ①. 到官网 https://www.bouncycastle.org/latest_releases.html 下载.jar工具包:

    ②.放到本机JDK的安装目录\jre\lib\ext 下面,而后即可经过前面的方法使用keytool生成BSK证书。

-end-
相关文章
相关标签/搜索