scheme http https 区别 tls_Android应用安全之 HTTPS 通信小结

本文资料从互联网收集,本人做了收集整理。文末有引用链接。

0x01 HTTPS 双向认证的握手流程

1b41ae64-fa17-eb11-8da9-e4434bdf6706.png

0x02 单向验证证书

证书锁定

预置的证书通常存储在 Android APP 的 raw文件夹中/res/raw/customtrustore.bks,也有时候会存储在/assets/文件夹下;还有时候硬编码在代码中。

APP代码内置仅接受指定域名的证书,而不接受操作系统或浏览器内置的CA根证书对应的任何证书,通过这种授权方式,保障了APP与服务端通信的唯一性和安全性,因此我们移动端APP与服务端(例如API网关)之间的通信是可以保证绝对安全。但是CA签发证书都存在有效期问题,所以缺点是在证书续期后需要将证书重新内置到APP中。

公钥锁定

预置公钥和预置证书类似,都存储在类似的位置。

公钥锁定则是提取证书中的公钥并内置到移动端APP中,通过与服务器对比公钥值来验证连接的合法性,我们在制作证书密钥时,公钥在证书的续期前后都可以保持不变(即密钥对不变),所以可以避免证书有效期问题。

证书指纹锁定

如果采用证书锁定方式,则获取证书的摘要hash,例如

## 在线读取服务器端.cer格式证书
openssl s_client -connect infinisign.com:443 -showcerts < /dev/null | openssl x509 -outform DER > infinisign.der
## 提取证书的摘要hash并查看base64格式
openssl dgst -sha256 -binary infinisign.der | openssl enc -base64
wLgBEAGmLltnXbK6pzpvPMeOCTKZ0QwrWGem6DkNf6o=

所以其中的wLgBEAGmLltnXbK6pzpvPMeOCTKZ0QwrWGem6DkNf6o=就是我们将要进行证书锁定的指纹(Hash)信息。

如果采用公钥锁定方式,则获取证书公钥的摘要hash,例如

## 在线读取服务器端证书的公钥
openssl x509 -pubkey -noout -in infinisign.der -inform DER | openssl rsa -outform DER -pubin -in /dev/stdin 2>/dev/null > infinisign.pubkey
## 提取证书的摘要hash并查看base64格式
openssl dgst -sha256 -binary infinisign.pubkey | openssl enc -base64
bAExy9pPp0EnzjAlYn1bsSEGvqYi1shl1OOshfH3XDA=

所以其中的bAExy9pPp0EnzjAlYn1bsSEGvqYi1shl1OOshfH3XDA=就是我们将要进行证书锁定的指纹(Hash)信息。

证书/公钥锁定例子

常规SSL证书设置

通常由CA权威机构签发的证书,其根证书都内置在最新的Android操作系统中,因此默认情况下可不进行SSL证书锁定,开发APP时也就变得非常简单,以infinisign.com为例,摘自App security best practices

JAVA方法

URL url = new URL("https://infinisign.com");
HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection();
urlConnection.connect();
InputStream in = urlConnection.getInputStream();

KOTLIN方法

val url = URL("https://infinisign.com")
val urlConnection = url.openConnection() as HttpsURLConnection
urlConnection.connect()
urlConnection.inputStream.use {
    ...
}

网络安全性设置

本方案是官方提供,但需要依赖Android N(Android 7.0 API 24)及以后版本,可在APP开发阶段在APP中内置安全性设置,以达到防止中间人攻击(MITM)的目的,此方法只限制在Android 7.0 API 24以后版本,因此该版本之前的安全性设置仍然需要使用证书锁定方法,本文以infinisign.com为例。

创建配置文件

创建文件res/xml/network_security_config.xml,需要注意的是,使用证书锁定,需要配置一个备份密钥,假如证书到期或更换了CA品牌后,不至于重新发行APP,这个备份密码可以是中级证书或根证书。

通俗的说,如果系统检测到签发证书过期了,则自动使用其中级或才根级证书作为验证,因为通常中级机构、根机构的证书到期时间非常长。

但实际情况是,http://infinisign.com所售的CA签发证书有效期都是一年,而现在发行APP或更新APP通常在一年都会有更新重新上架操作。

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <domain-config>
        <domain includeSubdomains="true">infinisign.com</domain>
        <pin digest="SHA-256">wLgBEAGmLltnXbK6pzpvPMeOCTKZ0QwrWGem6DkNf6o</pin>
        <!-- 备份密钥,比如infinisign.com的中级机构是geotrust -->
        <pin digest="SHA-256">wLgBEAGmLltnXbK6pzpvPMeOCTKZ0QwrWGem6DkNf6o</pin>
    </domain-config>
</network-security-config>

引入配置文件

Androidmanifest.xml引入配置文件android:networkSecurityConfig="@xml/network_security_config"

<?xml version="1.0" encoding="utf-8"?>
<manifest ... >
    <application
        android:networkSecurityConfig="@xml/network_security_config">
        <!-- application 其它子元素 -->
    </application>
</manifest>

OkHttp锁定

OkHttp是一个用于Android处理网络请求的开源项目,是安卓端最流行的轻量级的网络框架,其主要用来替代HttpUrlConnection处理方式

client = new OkHttpClient.Builder()
    .certificatePinner(new CertificatePinner.Builder()
      .add("infinisign.com", "sha256/S8Ff3JCaO4V...")
      .build())
    .build();

不过需要注意的是,OkHttp锁定证书方式不适用于公钥锁定方式,必须以证书锁定方式内置SSL数字证书。

TrustManager锁定

TrustManager是一个比较老的证书锁定方法,主要用于早期的Android版本或者用于一些CA根机构在Android系统中缺失根证书的情形下,当然也适用于自签名证书的锁定,通常我们不建议这样做,因为TrustManager的缺点是中间人仍然可以使用成熟的绕过方案来实现截持。

在SSL普及的今天,let's encrypt的开源免费解决方案,和一些入门的便宜DV域名型SSL证书(见PositiveSSL ¥39/年)足以媲美自签名方案。

详细请参考javax.net.ssl.TrustManager接口实现,简单步骤如下

  1. 在APP源码中内置证书

/res/raw

  1. 使用 KeyStore加载证书

kotlin val resourceStream = resources.openRawResource(R.raw.infinisign_cert) val keyStoreType = KeyStore.getDefaultType() val keyStore = KeyStore.getInstance(keyStoreType) keyStore.load(resourceStream, null)

  1. TrustManagerFactory实例化证书

kotlin val trustManagerAlgorithm = TrustManagerFactory.getDefaultAlgorithm() val trustManagerFactory = TrustManagerFactory.getInstance(trustManagerAlgorithm) trustManagerFactory.init(keyStore)

  1. 创建SSLContext实例,与TrustManager进行绑定。

kotlin val sslContext = SSLContext.getInstance("TLS") sslContext.init(null, trustManagerFactory.trustManagers, null) val url = URL("http://infinisign.com/") val urlConnection = url.openConnection() as HttpsURLConnection urlConnection.sslSocketFactory = sslContext.socketFactory

推荐使用:https://github.com/datatheorem/TrustKit-Android

小结

在多数移动操作系统中有大大小小几十个CA机构内置的根证书,但也不排除已经不被信任的CA机构存在的旧根证书,还有一些例如国内的一些基于Android老版本的操作系统仍然面临着安全风险,所以使用SSL数字证书锁定(SSL/TLS Pinning)的目标是缩小可信CA的范围,让APP客户端传送数据更安全,更切实地保障用户数据。

0x03 双向认证证书

预置服务器证书和客户端证书

基于OkHttp + Retrofit + Rxjava实现 HTTPS 双向认证

基于Retrofit实现HTTPS思路

由于Retrofit是基于OkHttp实现的,因此想通过Retrofit实现HTTPS需要给Retrofit设置一个OkHttp代理对象用于处理HTTPS的握手过程。代理代码如下:

OkHttpClient okHttpClient = new OkHttpClient.Builder()
    .sslSocketFactory(SSLHelper.getSSLCertifcation(context))//为OkHttp对象设置SocketFactory用于双向认证
    .hostnameVerifier(new UnSafeHostnameVerifier())
    .build();
Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://10.2.8.56:8443")
    .addConverterFactory(GsonConverterFactory.create())//添加 json 转换器
    .addCallAdapterFactory(RxJavaCallAdapterFactory.create())//添加 RxJava 适配器
    .client(okHttpClient)//添加OkHttp代理对象
    .build();

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获取SSLFactory实例进行网络访问

private void fetchData() {
  OkHttpClient okHttpClient = new OkHttpClient.Builder()
      .sslSocketFactory(SSLHelper.getSSLCertifcation(context))//获取SSLSocketFactory
      .hostnameVerifier(new UnSafeHostnameVerifier())//添加hostName验证器
      .build();
 
  Retrofit retrofit = new Retrofit.Builder()
       .baseUrl("https://10.2.8.56:8443")//填写自己服务器IP
       .addConverterFactory(GsonConverterFactory.create())//添加 json 转换器
       .addCallAdapterFactory(RxJavaCallAdapterFactory.create())//添加 RxJava 适配器
       .client(okHttpClient)
       .build();
 
  IUser userIntf = retrofit.create(IUser.class);
   
  userIntf.getUser(user.getPhone())
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread()) 
        .subscribe(new Subscriber<UserBean>() {
                //省略onCompleted、onError、onNext
        }
  });
}
private class UnSafeHostnameVerifier implements HostnameVerifier {
  @Override
  public boolean verify(String hostname, SSLSession session) {
      return true;//自行添加判断逻辑,true->Safe,false->unsafe
  }
}

基于SslSocketFactory方式双向认证

private static final String KEY_STORE_TYPE_BKS = "bks";//证书类型 固定值
    private static final String KEY_STORE_TYPE_P12 = "PKCS12";//证书类型 固定值

    private static final String KEY_STORE_CLIENT_PATH = "client.p12";//客户端要给服务器端认证的证书
    private static final String KEY_STORE_TRUST_PATH = "client.truststore";//客户端验证服务器端的证书库
    private static final String KEY_STORE_PASSWORD = "123456";// 客户端证书密码
    private static final String KEY_STORE_TRUST_PASSWORD = "123456";//客户端证书库密码

    /**
     * 获取SslSocketFactory
     *
     * @param context 上下文
     * @return SSLSocketFactory
     */
    public static SSLSocketFactory getSslSocketFactory(Context context) {
        try {
            // 服务器端需要验证的客户端证书
            KeyStore keyStore = KeyStore.getInstance(KEY_STORE_TYPE_P12);
            // 客户端信任的服务器端证书
            KeyStore trustStore = KeyStore.getInstance(KEY_STORE_TYPE_BKS);

            InputStream ksIn = context.getResources().getAssets().open(KEY_STORE_CLIENT_PATH);
            InputStream tsIn = context.getResources().getAssets().open(KEY_STORE_TRUST_PATH);
            try {
                keyStore.load(ksIn, KEY_STORE_PASSWORD.toCharArray());
                trustStore.load(tsIn, KEY_STORE_TRUST_PASSWORD.toCharArray());
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                try {
                    ksIn.close();
                } catch (Exception ignore) {
                }
                try {
                    tsIn.close();
                } catch (Exception ignore) {
                }
            }
            return new SSLSocketFactory(keyStore, KEY_STORE_PASSWORD, trustStore);
        } catch (KeyManagementException | UnrecoverableKeyException | KeyStoreException | FileNotFoundException | NoSuchAlgorithmException | ClientProtocolException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 获取SSL认证需要的HttpClient
     *
     * @param context 上下文
     * @param port    端口号
     * @return HttpClient
     */
    public static HttpClient getSslSocketFactoryHttp(Context context, int port) {
        HttpClient httpsClient = new DefaultHttpClient();
        SSLSocketFactory sslSocketFactory = getSslSocketFactory(context);
        if (sslSocketFactory != null) {
            Scheme sch = new Scheme("https", sslSocketFactory, port);
            httpsClient.getConnectionManager().getSchemeRegistry().register(sch);
        }
        return httpsClient;
    }

基于SSLContext双向认证代码

private static final String KEY_STORE_TYPE_BKS = "bks";//证书类型 固定值
    private static final String KEY_STORE_TYPE_P12 = "PKCS12";//证书类型 固定值

    private static final String KEY_STORE_CLIENT_PATH = "client.p12";//客户端要给服务器端认证的证书
    private static final String KEY_STORE_TRUST_PATH = "client.truststore";//客户端验证服务器端的证书库
    private static final String KEY_STORE_PASSWORD = "123456";// 客户端证书密码
    private static final String KEY_STORE_TRUST_PASSWORD = "123456";//客户端证书库密码

    /**
     * 获取SSLContext
     *
     * @param context 上下文
     * @return SSLContext
     */
    private static SSLContext getSSLContext(Context context) {
        try {
            // 服务器端需要验证的客户端证书
            KeyStore keyStore = KeyStore.getInstance(KEY_STORE_TYPE_P12);
            // 客户端信任的服务器端证书
            KeyStore trustStore = KeyStore.getInstance(KEY_STORE_TYPE_BKS);

            InputStream ksIn = context.getResources().getAssets().open(KEY_STORE_CLIENT_PATH);
            InputStream tsIn = context.getResources().getAssets().open(KEY_STORE_TRUST_PATH);
            try {
                keyStore.load(ksIn, KEY_STORE_PASSWORD.toCharArray());
                trustStore.load(tsIn, KEY_STORE_TRUST_PASSWORD.toCharArray());
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                try {
                    ksIn.close();
                } catch (Exception ignore) {
                }
                try {
                    tsIn.close();
                } catch (Exception ignore) {
                }
            }
            SSLContext sslContext = SSLContext.getInstance("TLS");
            TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
            trustManagerFactory.init(trustStore);
            KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("X509");
            keyManagerFactory.init(keyStore, KEY_STORE_PASSWORD.toCharArray());
            sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null);
            return sslContext;
        } catch (Exception e) {
            Log.e("tag", e.getMessage(), e);
        }
        return null;
    }

    /**
     * 获取SSL认证需要的HttpClient
     *
     * @param context 上下文
     * @return OkHttpClient
     */
    public static OkHttpClient getSSLContextHttp(Context context) {
        OkHttpClient client = new OkHttpClient();
        SSLContext sslContext = getSSLContext(context);
        if (sslContext != null) {
            client.setSslSocketFactory(sslContext.getSocketFactory());
        }
        return client;
    }

    /**
     * 获取HttpsURLConnection
     *
     * @param context 上下文
     * @param url     连接url
     * @param method  请求方式
     * @return HttpsURLConnection
     */
    public static HttpsURLConnection getHttpsURLConnection(Context context, String url, String method) {
        URL u;
        HttpsURLConnection connection = null;
        try {
            SSLContext sslContext = getSSLContext(context);
            if (sslContext != null) {
                u = new URL(url);
                connection = (HttpsURLConnection) u.openConnection();
                connection.setRequestMethod(method);//"POST" "GET"
                connection.setDoOutput(true);
                connection.setDoInput(true);
                connection.setUseCaches(false);
                connection.setRequestProperty("Content-Type", "binary/octet-stream");
                connection.setSSLSocketFactory(sslContext.getSocketFactory());
                connection.setConnectTimeout(30000);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return connection;
    }

0x04 证书间转换

使用支持openSSL命令的工具可执行以下命令进行转换

1、DER用于二进制DER编码证书,可以是CER/CRT的表现形式,类似下面的形式

3082 05f7 3082 04df a003 0201 0202 1100
ddd1 cf29 a1bc 4b6d c7ba 2a81 170c 5644
300d 0609 2a86 4886 f70d 0101 0b05 0030
8196 310b 3009 0603 5504 0613 0247 4231

2、CER/CRT等证书格式不含私钥,CER常用于Windows操作系统扩展名,CRT常用于Unix类的系统,例如MacOS或者Linux

x509(cer/crt/pem等)转为pfx格式

如果转换需要设置密码,请输入密码,如不需要则留空

openssl pkcs12 -export -in server.pem -inkey server_private.key -out server.pfx

crt转为pem

openssl x509 -in server.crt -out server.der -outform DER
openssl x509 -in server.der -inform DER -out server.pem -outform PEM

pfx转为pem

openssl pkcs12 -in server.pfx -out server.pem -nodes

pfx提取私钥和公钥(key)

从pfx提取密钥对信息,并转换为key格式(使用pkcs12补全,如果pfx加密,提示输入密码输入即可)

openssl pkcs12 -in server.pfx -nocerts -nodes -out server.key

从密钥对提取私钥

openssl rsa -in server.key -out server_private.key

从密钥对提取公钥

openssl rsa -in server.key -pubout -out server_public.key

如果因为RSA算法使用的是pkcs8模式,需要对提取的私钥使用下述命令

openssl pkcs8 -in server_private.key -out server_private.p8 -outform der -nocrypt -topk8

key转为pvk

pvk是微软专用的用于保存私钥的文件格式

openssl rsa -in server.key -pvk-strong -out server.pvk

x509(cer/crt/pem等)转为spc

spc是微软专用的用于保存公钥的文件格式

openssl crl2pkcs7 -nocrl -certfile server.pem  -outform DER -out server.spc

参考

https://www. infinisign.com/faq/what -is-ssl-pinning https://www. infinisign.com/faq/andr oid-ssl-pinning https://www. infinisign.com/faq/ssl- tls-transfer https:// developer.android.com/t raining/articles/security-config https:// mobilefirstplatform.ibmcloud.com /tutorials/zh-hans/foundation/8.0/authentication-and-security/certificate-pinning/ https://www. jianshu.com/p/64172ccfb 73b http:// frank-zhu.github.io/and roid/2014/12/26/android-https-ssl/ http:// frank-zhu.github.io/and roid/2017/03/30/android-https-ssl-part-02/ (Android HTTPS SSL双向验证(CA根证书))

http://weixin.qq.com/r/pi1hef7EMqNNrboa93i0 (二维码自动识别)

了解更多的网安知识请动动你的小手关注下公众号!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值