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

时间:2022-06-18 12:58:57

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

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

  代入场景:假设现在 A 要与远端的 B 建立安全的连接进行通信。

  1. 直接使用对称加密通信,那么密钥无法安全的送给 B 。
  2. 直接使用非对称加密,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. 证书类型:

JKS:数字证书库。JKS里有KeyEntry和CertEntry,在库里的每个Entry都是靠别名(alias)来识别的。
P12:是PKCS12的缩写。同样是一个存储私钥的证书库,由.jks文件导出的,用户在PC平台安装,用于标示用户的身份
CER:俗称数字证书,目的就是用于存储公钥证书,任何人都可以获取这个文件 。
BKS:由于Android平台不识别.keystore.jks格式的证书库文件,因此Android平台引入一种的证书库格式,BKS。
下图展示了证书的使用流程:
Android : 关于HTTPS、TLS/SSL认证以及客户端证书导入方法
为什么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工具包:
Android : 关于HTTPS、TLS/SSL认证以及客户端证书导入方法

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

-end-