crazyandcoder

Android自制HTTPS证书实现双向认证

2021.07.16

1 前言

前一阶段学习了服务端的知识,然后就写了一套接口,然后在阿里云上面租了一个服务器将服务部署到服务器上面,由于域名一直没有备案成功,所以只能通过 ip 的方式进行访问。通过 ip 的方式其实也没什么大不了的,前阶段工作中还简单实现了一套 HTTPDNS,原理其实也是通过 ip 直连的方式进行接口访问。本人是 Android 端开发,写了一套接口,然后就简单实现了一个 app,将前后端打通,练练手。心血来潮,在 Google 注册了一个开发者账号,将应用上传到 GooglePlay 市场,如果有兴趣的话,后期简单写一篇关于上传应用到 Googleplay 市场的文章。上传 GooglePlay 应用市场时出现了问题,我用 ip 直连,但是是 http 请求,不是 https 的请求。现阶段基本上都是 https 了,但是因为是个人写着玩,练练手的,也没那么多钱去买个正规的 CA 证书,所以就自己动手制作了一个个人证书,接下来将详细介绍,如何在 Android 端下和服务端实现自制证书的连接。

2 CA 证书

CA 证书是由 CA(Certification Authority)机构发布的数字证书。其内容包含:电子签证机关的信息、公钥用户信息、公钥、签名和有效期。这里的公钥服务端的公钥,这里的签名是指:用 hash 散列函数计算公开的明文信息的信息摘要,然后采用 CA 的私钥对信息摘要进行加密,加密完的密文就是签名。 即:证书 = 公钥 + 签名 +申请者和颁发者的信息。 客户端中因为在操作系统中就预置了 CA 的公钥,所以支持解密签名

2.1 制作证书

进入实战操作,生成我们自己的证书。在生成证书的过程中,会涉及到证书一些格式,下面简单介绍一下:

  1. JKS:数字证书库。JKS 里有 KeyEntry 和 CertEntry,在库里的每个 Entry 都是靠别名(alias)来识别的。
  2. P12:是 PKCS12 的缩写。同样是一个存储私钥的证书库,由 .jks 文件导出的,用户在 PC 平台安装,用于标示用户的身份。
  3. CER:俗称数字证书,目的就是用于存储公钥证书,任何人都可以获取这个文件 。
  4. BKS:由于 Android 平台不识别 .keystore 和 .jks 格式的证书库文件,因此 Android 平台引入一种的证书库格式,BKS。

客户端需要用到的是 client.p12 (客户端证书,用于请求的时候给服务器来验证身份之用)和 client.truststore (客户端证书库,用于验证服务器端身份,防止钓鱼)这两个文件.其中安卓端的证书类型必须要求是 BKS 类型,具体生成可以参考这个 create a bks bouncycastle,这里涉及到这个 JARbcprov-ext-jdk15on-152.jar 文件.

注意:对于以下括号括起来的需要换成你自己的数据

2.2 生成客户端 keystore

keytool -genkeypair -alias (AAAAA) -keyalg RSA -validity (BBBBB) -keypass (CCCCC) -storepass (DDDDD) -keystore (EEEEE)
1. (AAAAA)别名,随便填写,参考值:client
2. (BBBBB)此处的需要填写证书有效期,最好时间长点,可以选择25年以上,不过单位是天,所以可以天365*25=9125(天),参考值:9125
3. (CCCCC)密码,参考值:123456
4. (DDDDD)密码,参考值:123456
5. (EEEEE)keystore保存的地址,参考值:/my/keystore/client.jks

2.3 生成服务器 keystore

keytool -validity (AAAAA) -genkey -v -alias (BBBBB) -keyalg RSA -keystore (CCCCC) -dname (DDDDD) -storepass (EEEEE) -keypass (FFFFF)
1. (AAAAA)此处的需要填写证书有效期,最好时间长点,可以选择25年以上,不过单位是天,所以可以天365*25=9125(天),参考值:9125
2. (BBBBB)别名,随便填写,参考值:server
3. (CCCCC)keystore保存的地址,参考值:/my/keystore/server.keystore
4. (DDDDD)参考值:"CN=服务器ip地址,OU=android,O=android,L=Shanghai,ST=Shanghai,c=cn"
5. (EEEEE)密码,参考值:123456
6. (FFFFF)密码,参考值:123456

其中需要注意的是第四点,CN需要填写服务器ip地址。

2.4 生成客户端证书库

keytool -validity (AAAAA) -genkeypair -v -alias (BBBBB) -keyalg RSA -storetype PKCS12 -keystore (CCCCC) -dname (DDDDD) -storepass (EEEEE) -keypass (FFFFF)
1. (AAAAA)此处的需要填写证书有效期,最好时间长点,可以选择25年以上,不过单位是天,所以可以天365*25=9125(天),参考值:9125
2. (BBBBB)客户端别名,参考值:client
3. (CCCCC)P12存放地址,参考值:/my/keystore/client.p12
4. (DDDDD)参考值:"CN=client,OU=android,O=android,L=Shanghai,ST=Shanghai,c=cn"
5. (EEEEE)密码,参考值:123456
6. (FFFFF)密码,参考值:123456

2.5 从客户端证书库中导出客户端证书

keytool -export -v -alias (AAAAA ) -keystore (BBBBB) -storetype PKCS12 -storepass (CCCCC) -rfc -file (DDDDD)
1. (AAAAA)参考值:client
2. (BBBBB)参考值:/my/keystore/client.p12 
3. (CCCCC)参考值:123456
4. (DDDDD)参考值:/my/keystore/client.cer

2.6 从服务器证书库中导出服务器证书

keytool -export -v -alias (AAAAA) -keystore (BBBBB) -storepass (CCCCC) -rfc -file (DDDDD)
1. (AAAAA)参考值:server
2. (BBBBB)参考值:/my/keystore/server.keystore
3. (CCCCC)参考值:123456
4. (DDDDD)参考值:/my/keystore/server.cer

2.7 生成客户端信任证书库(由服务端证书生成的证书库)

keytool -import -v -alias (AAAAA) -file (BBBBB) -keystore (CCCCC) -storepass (DDDDD) -storetype BKS -provider org.bouncycastle.jce.provider.BouncyCastleProvider -providerpath (EEEEE)
1. (AAAAA)参考值:server
2. (BBBBB)参考值:/my/keystore/server.cer
3. (CCCCC)参考值:/my/keystore/truststore.jks
4. (DDDDD)参考值:123456
5. (EEEEEE)参考值:/my/keystore/bcprov-ext-jdk15on-166.jar,下载地址(jar地址)

2.8 将客户端证书导入到服务器证书库(使得服务器信任客户端证书)

keytool -import -v -alias (AAAAA) -file (BBBBB) -keystore (CCCCC) -storepass (DDDDD)
1. (AAAAA)参考值:client
2. (BBBBB)参考值:/my/keystore/client.cer
3. (CCCCC)参考值:/my/keystore/server.keystore
4. (DDDDD)参考值:123456

2.9 查询证书库中的全部证书

keytool -list -keystore (AAAAA) -storepass (BBBBBB)
1. (AAAAA)参考值:/my/keystore/server.keystore
2. (BBBBB)参考值:123456

3 认证

3.1 单向认证

单向认证大体意思就是客户端只验证服务端的合法性。即服务器保存公钥证书和私钥两个文件,客户端获取这个证书里面的公钥生成私钥,服务器端用自己的私钥去解密这个客户端发过来的私钥,后续的 https 通信过程就用这个私钥进行加密。

单向认证过程.png

3.2 双向认证

对于双向证书验证,也就是说,客户端持有服务端的公钥证书,并持有自己的私钥,服务端持有客户的公钥证书,并持有自己私钥,建立连接的时候,客户端利用服务端的公钥证书来验证服务器是否上是目标服务器;服务端利用客户端的公钥来验证客户端是否是目标客户端。 服务端给客户端发送数据时,需要将服务端的证书发给客户端验证,验证通过才运行发送数据,同样,客户端请求服务器数据时,也需要将自己的证书发给服务端验证,通过才允许执行请求。

双向认证过程.png

4 使用

4.1 生成BKS文件

Java 平台默认识别 jks 格式的证书文件,但是 android 平台只识别 bks 格式的证书文件。因此需要将上面生成的 jks 文件转成 bks 格式的,使用工具通过 portecle 下载地址 来执行。

4.2 转换步骤:

  1. 下载上述的 jar 包放到桌面上
  2. 运行这个 jar 文件,执行语句:java -jar /desktop/protecle.jar,此时会弹出 portecle 工具

在这里插入图片描述

  1. 运行 protecle.jar 将 client.jks 和 truststore.jks 分别转换成 client.bks 和 truststore.bks,然后放到 android 客户端的 assert 目录下
1.点击File
2.选择open Keystore File
3.选择上面的client.jks
4.输入密码(123456)
5.选择Tools
6.选择change keystore type
7.选择BKS
8.选择save keystore as
9.选择保存即可

//重复上述步骤,生成truststore.bks文件

4.3 Android 端接入

目前 Android 使用的主流网络框架一般是 Okhttp 或者 Retrofit,不管是哪种网络请求框架,它们都提供了一个设置入口,我们以 OkHttp 为例:

4.3.1 创建 assert 目录

在 Android 工程下面创建一个 assert 目录,然后将上述生成的 client.bks 和 truststore.bks 文件放进去

4.3.2 创建自定义 SSLSocketFactory

public class SSLHelper {

    private static String CLIENT_PRI_KEY = "client.bks";
    private static String TRUSTSTORE_PUB_KEY = "truststore.bks";
    private static String CLIENT_BKS_PASSWORD = "123456";
    private 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.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 (Exception e) {
            e.printStackTrace();
        }
        return sslSocketFactory;
    }
}

上述代码就是读取 assert 目录下面的 bks 文件,然后设置到 OkHttp 中去:

      //OkHttp
      OkHttpClient okHttpClient = new OkHttpClient.Builder()
      //获取SSLSocketFactory
      .sslSocketFactory(SSLHelper.getSSLCertifcation(context))
      //添加hostName验证器
      .hostnameVerifier(new UnSafeHostnameVerifier())
      .build();


       //Retrofit
       Retrofit retrofit = new Retrofit.Builder()
       .baseUrl("baseUrl")
       .addConverterFactory(GsonConverterFactory.create())
       .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
       .client(okHttpClient)
       .build();

以上便是 Android 端的设置步骤。接着我们设置服务端的配置

4.4 Java 服务端配置

服务端配置非常简单,只是简单的配置项即可。将第三步生成的 server.keystore 放到 resources 目录下面即可。然后在 application.properties 这个配置文件中加入以下代码即可:

server.ssl.enabled=true
server.ssl.key-store=classpath:server.keystore
server.ssl.key-store-password=123456
server.ssl.key-alias=server
server.ssl.keyStoreType=JKS
server.ssl.trust-store=classpath:server.keystore
server.ssl.trust-store-password=123456
server.ssl.client-auth=need
server.ssl.trust-store-type=JKS
server.ssl.trust-store-provider=SUN

5 总结

经过以上所有的步骤,我们便可以在自己的项目中使用自制的 https 证书了,以前是通过 http 访问的,现在是通过 https 访问,大大增强了数据传输的安全性。