Android 应用实现 Https 双向认证

3035次阅读  |  发布于2年以前

为什么需要双向认证

Https保证的是信道的安全,即客户端和服务端通信报文的安全。但是无法保证中间人攻击,所以双向认证解决的问题就是防止中间人攻击。

中间人攻击(Man-in-the-MiddleAttack)简称(MITM),是一种“间接”的入侵攻击,这种攻击模式是通过各种技术手段将受入侵者控制的一台计算机虚拟放置在网络连接中的两台通信计算机之间,这台计算机就称为“中间人”。若没有开启双向认证,中间人可以拦截客户端发送的请求,然后篡改信息再发送到服务端;中间人也可以拦截服务端返回的信息,再发送到客户端。所以使用Https的单向认证或双向认证能够有效防止中间人攻击。

注:无论Ca证书还是自签证书都需要双向认证。

双向认证原理

1、服务端认证客户端原理

客户端有自己的bks证书auth_client.bks,并将导出的auth_client_pub.cer证书导入到服务端证书auth_server.keystore中,这样服务端就将客户端证书添加到信任列表中,从而能够让带有该auth_client_pub.cer证书信息的客户端访问服务。

2、客户端认证服务端原理

服务端有自己的证书(ca颁发的或者是自己创建的)auth_server.keystore,并导出auth_server_pub.cer证书,将该证书导入到客户端证书

auth_truststore.jks中,注意:这里不是导入到auth_client.jks中,而是导入生成另一个证书auth_truststore.jks,最后再将jks证书转化成bks证书。

实现过程

一、服务端证书

创建服务端证书

keytool -genkeypair -alias auth_server -keyalg RSA -validity 36500 -keypass auth_server -storepass auth_server -keystore /Users/renzhongrui/android/certs/auth_server.keystore

导出服务端证书公钥

keytool -export -alias auth_server -file /Users/renzhongrui/android/certs/auth_server_pub.cer -keystore /Users/renzhongrui/android/certs/auth_server.keystore -storepass auth_server

二、客户端证书

创建客户端证书(andoird不能用keystore格式的密钥库,所以先生成jks格式,再用Portecle工具转成bks格式)

keytool -genkeypair -alias auth_client -keyalg RSA -validity 36500 -keypass auth_client -storepass auth_client -keystore /Users/renzhongrui/android/certs/auth_client.jks

导出客户端证书公钥

keytool -export -alias auth_client -file /Users/renzhongrui/android/certs/auth_client_pub.cer -keystore /Users/renzhongrui/android/certs/auth_client.jks -storepass auth_client 

三、证书交换

将客户端证书导入服务端keystore中,再将服务端证书导入客户端auth_truststore中, 一个keystore可以导入多个证书,生成证书列表。

将客户端公钥导入到服务端keystore证书中,使得服务端能够信任客户端。

keytool -import -v -alias auth_client -file /Users/renzhongrui/android/certs/auth_client_pub.cer -keystore /Users/renzhongrui/android/certs/auth_server.keystore -storepass auth_server

生成客户端信任证书库auth_truststore.jks,即将服务端公钥导入到客户端jks证书中,使得客户端能够信任服务端。

keytool -import -v -alias auth_server -file /Users/renzhongrui/android/certs/auth_server_pub.cer -keystore /Users/renzhongrui/android/certs/auth_truststore.jks -storepass auth_truststore

最后验证一下,查看证书库中的所有证书

keytool -list -keystore /Users/renzhongrui/android/certs/auth_server.keystore -storepass auth_server
keytool -list -keystore /Users/renzhongrui/android/certs/auth_truststore.jks -storepass auth_truststore

四、证书转换

下载portecle.jar(https://sourceforge.net/projects/portecle/),解压后运行jar包:-

java -jar portecle.jar

1、点击File菜单选择Open Keystore File,选择创建好的auth_client.jks或auth_truststore.jks证书,输入密码。
2、选中导入的证书,点击Tools菜单,选择Change Keystore Type,再选择BKS类型,再次输入密码,确认之后,会显示成功。
3、最后点击File菜单,选择Save Keystore File As,将证书保存的指定路径。

五、配置服务

证书准备好之后,就可以进行集成测试了,服务使用Spring Boot创建或者使用Nginx代理。

使用Spring Boot服务

1、添加配置

server:
  port: 443
  server:
    tomcat:
      uri-encoding: UTF-8
  # 开启https,配置跟证书对应
  ssl:
    key-store: classpath:auth_server.keystore
    key-store-type: JKS
    key-store-password: auth_server
    key-password: auth_server
    key-alias: auth_server
    enabled: true
    #是否需要进行认证
    client-auth: need
    protocol: TLS # 默认
    trust-store: classpath:auth_server.keystore
    trust-store-password: auth_server
    trust-store-type: JKS

2、添加代码,这里配置80端口重定向到443,也可以改成别的端口。

public class PackApplication implements WebMvcConfigurer {

    public static void main(String[] args) {
        SpringApplication.run(PackApplication.class, args);
    }

    @Bean
    public Connector connector(){
        Connector connector=new Connector("org.apache.coyote.http11.Http11NioProtocol");
        connector.setScheme("http");
        connector.setPort(80);
        connector.setSecure(false);
        connector.setRedirectPort(443);
        return connector;
    }

    @Bean
    public TomcatServletWebServerFactory tomcatServletWebServerFactory(Connector connector){
        TomcatServletWebServerFactory tomcat=new TomcatServletWebServerFactory(){
            @Override
            protected void postProcessContext(Context context) {
                SecurityConstraint securityConstraint=new SecurityConstraint();
                securityConstraint.setUserConstraint("CONFIDENTIAL");
                SecurityCollection collection=new SecurityCollection();
                collection.addPattern("/*");
                securityConstraint.addCollection(collection);
                context.addConstraint(securityConstraint);
            }
        };
        tomcat.addAdditionalTomcatConnectors(connector);
        return tomcat;
    }

}

使用Nginx服务配置

Nginx配置与Spring Boot服务配置略有不同。

server {
        listen       443;
        server_name  192.168.200.101; # 代理服务IP
        ssl on; # 开启Https

        ssl_certificate      /usr/local/nginx/conf/https/auth_server.cer; # auth_server.keystore导出的cer证书
        ssl_certificate_key  /usr/local/nginx/conf/https/auth_server.key; # auth_server.keystore导出的私钥
        ssl_client_certificate /usr/local/nginx/conf/https/auth_client.cer; # auth_client.keystore导出的cer

        ssl_session_cache    shared:SSL:1m;
        ssl_session_timeout  5m;
        ssl_verify_client optional; # 配置校验客户端策略,设置成optional时候可以开启白名单接口

        ssl_protocols TLSv1.1 TLSv1.2;

        ssl_ciphers  HIGH:!aNULL:!MD5;
        ssl_prefer_server_ciphers  off;

        location / { # 需要双向验证https的接口
            if ($ssl_client_verify != SUCCESS) {
                 return 401;
            }
            proxy_pass http://192.168.200.101:8008;
            proxy_connect_timeout 600;
            proxy_read_timeout 600;
        }

        location /aarm/downloadUpdateFile { # 获取证书版本和下载证书接口,不需要验证Https
            proxy_pass http://192.169.200.101:8008;
            proxy_connect_timeout 600;
            proxy_read_timeout 600;
        }
    }

六、配置客户端

在客户端app中使用OkHttp来进行网络访问,所以需要配置OkHttp来进行证书认证。

1、将上面创建的auth_client.bks和auth_truststore.bks证书放到assets目录下
2、初始化OkHttp

OkHttpClient okHttpClient = new OkHttpClient.Builder()
  .connectTimeout(10, TimeUnit.SECONDS)
  .readTimeout(10, TimeUnit.SECONDS)
  .sslSocketFactory(Https.getSSLCertifcation(getApplicationContext()))//获取SSLSocketFactory
  .hostnameVerifier(new UnSafeHostnameVerifier())//添加hostName验证器
  .build();

重点需要看一下Https类的实现:

public class Https {

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

还有一个UnSafeHostnameVerifier类

private class UnSafeHostnameVerifier implements HostnameVerifier {
  @Override
  public boolean verify(String hostname, SSLSession session) {
    return true;
  }
}

此时再进行网络请求,就能够访问到带有双向认证的服务端接口了。当然一般网上的博客到这就结束了,但是这样真的就完事了吗,其实真正的设计才刚开始,如果只是了解原理,读到这里就可以了,下面才是真实应用场景。

真实场景实现

原理还是那个原理,就看怎么合理的使用了。在真实开发环境中,需要解决几个问题:
auth_client.bks和auth_truststore.bks是需要动态下发的不是所有的接口都需要进行双向认证

动态下发auth_client.bks和auth_truststore.bks

1、auth_client.bks和auth_truststore.bks的制作需要在本地工具完成,然后通过管理端上传到服务器,并且改变证书的版本号;
2、客户端需要访问证书版本,来判断是否需要更新证书,如果需要更新则下载证书。

这里会引出两个问题:

1、请求版本号的接口和下载证书的接口不能进行双向认证,否则无法下发证书。
2、不进行双向认证的接口是不安全的,所以,请求版本号的接口的返回值是需要加密的;

针对第一个问题处理方式:

服务端需要配置白名单,将请求版本号的接口和下载证书的接口过滤掉;

客户端OkHttp首次初始化不能进行双向认证,等下载完证书之后,需要再次进行OkHttp初始化;

针对第二个问题处理方式:

需要本地工具创建RSA公私钥对,用于请求版本号接口的加解密;

服务端使用私钥对报文加密,客户端保存公钥,并使用公钥对报文解密。

客户端使用公钥解密后的报文格式:

{
    "version":1,
    "authType":2,
    "clientBksPath":"https://localhost/downloadUpdateFile?fileName=auth_client.bks",
    "trustBksPath":"https://localhost/downloadUpdateFile?fileName=auth_truststore.bks",
    "authKey":"auth_client"
}

version: 表示每一次更换证书的版本;

authType:0 表示不开启认证,1 表示开启单向认证,2 表示开启双向认证clientBksPath:auth_client.bks下载路径trustBksPath:auth_truststore.bks下载路径authKey:auth_client.bks证书密码客户端每次启动都要获取服务端证书版本,并将证书信息存储到本地文件或者数据库中,通过对比服务端证书版本和数据库中版本来判断是否需要证书更新。

注:这样设计的好处是当证书过期时,能够动态下发证书,但会引出一个问题,客户端要安全的存储公钥信息,一般做法是将公钥存储到so文件里,再配合应用加固手段进行保护,不过这个就不是通信安全的问题了,而是apk安全的问题。

其他证书操作

1、查看keystore证书公钥-

keytool -list -rfc --keystore release.keystore | openssl x509 -inform pem -pubkey

2、查看keystore证书私钥

先转成pfx格式

keytool -v -importkeystore -srckeystore release.keystore -srcstoretype jks -srcstorepass 123456 -destkeystore keystore/release.pfx -deststoretype pkcs12 -deststorepass 123456 -destkeypass 123456

再查看证书私钥

openssl pkcs12 -in release.pfx -nocerts -nodes

Copyright© 2013-2019

京ICP备2023019179号-2