1.理论

1.1术语

公钥加密 : 加密
私钥解密 : 解密
私钥加密 : 签名
公钥解密 : 验证签名

非对称加密:即常见的 RSA 算法,还包括 ECC、DH 等算法。算法特点是,密钥成对出现,一般称为公钥(公开)和私钥(保密),公钥加密的信息只能私钥解开,私钥加密的信息只能公钥解开。因此掌握公钥的不同客户端之间不能互相解密信息,只能和掌握私钥的服务器进行加密通信,服务器可以实现1对多的通信,客户端也可以用来验证掌握私钥的服务器身份。非对称加密的特点是信息传输1对多,服务器只需要维持一个私钥就能够和多个客户端进行加密通信,但服务器发出的信息能够被所有的客户端解密,且该算法的计算复杂,加密速度慢。

1.2https必要性

HTTPS自签名证书以及Android应用https请求_Android 图1

如图1所示使用http协议有信息被窃听、信息被篡改、信息被劫持等风险。
而使用https协议则可以通过身份验证、信息加密、完整性校验等来解决这些问题。
HTTPS在HTTP的基础上加入了SSL协议,SSL依靠证书来验证服务器的身份,并为客户端和服务器之间的通信加密。

1.3https请求原理

HTTPS自签名证书以及Android应用https请求_Android_02 图2

如图2所示客户端与服务端进行https请求时
首先通过非对称加密方式进行身份认证、交互证书、协商通信内容的对称加密秘钥等
然后使用对称加密来进行通信,并通过散列算法防止信息篡改和验证通信内容的完整性。

1.4https请求过程

HTTPS自签名证书以及Android应用https请求_Android_03 图3

1.客户端发送SSL协议版本信息、加密套件候选列表、压缩算法候选列表、随机数random_C、扩展字段等信息
2.服务端回应选择使用的协议版本 version、选择的加密套件 cipher suite、选择的压缩算法 compression method、随机数 random_S 等、对应的证书链、是否双向认证client_certificate_request
3.客户端验证服务端证书的有效性,然后生成随机数Pre-master,再根据随机数random_C、random_S计算生成协商秘钥
4.客户端发送用服务端证书公钥加密的随机数字 Pre-master、通信的对称加密算法和密钥、所有通信参数生成的 hash 值等
5.客户端根据随机数Pre-master、random_C、random_S计算生成协商秘钥,并计算所有通信参数生成的 hash 值,用协商秘钥解密客户端发送过来的hash值,验证服务端hash值是否与客户端hash值一致
6.服务端告知客户端后续的通信都采用协商的密钥与算法进行加密通信,结合所有当前的通信参数信息生成一段数据
采用协商密钥 session secret 与算法加密并发送到客户端

1.5证书的签发的必要

HTTPS自签名证书以及Android应用https请求_Android_04 图4

如图4所示,如果签发服务器的证书不可靠,则会引起中间人Middle的劫持、窃听、篡改
中间人Middle有一个自己的证书m_pub_key以及私钥m_priv_key
客户端Client请求信息被中间人Middle劫持后,中间人Middle模拟客户端Client发送请求信息给服务端Server
服务端Server响应中间人Middle的请求,将服务端Server的证书s_pub_key传递给中间人Middle
中间人Middle模拟服务端Server将自己的证书m_pub_key传递给客户端Client
客户端Client接收到m_pub_key后,认为其是正在的服务端Server证书,则就会将自己的敏感信息c_info发送给中间人Middle了
中间人Middle根据客户端Client的信息c_info,进行篡改变成了自己的信息m_info发送给正在的服务端Server了
服务端Server响应中间人Middle请求,将结果s_info返回给中间人Middle;中间人Middle再将服务端信息s_info进行修改变成了m_info 返回给客户端Client
这个过程中客户端Client与服务端Server都没有发现它们之间的通信都被劫持、窃听、篡改了

因此需要一个公证机构CA来进行保证,CA会根据服务端Server的信息(网站域名等)使用CA自己的私钥ca_priv_key来进行签名,客户端Client使用公证机构CA的证书ca_pub_key来验证签名,从而确认客户端Client访问的服务端Server获得的证书,是真正有效的证书,进而有效的避免了中间人Middle的攻击。

1.6对称秘钥使用

HTTPS自签名证书以及Android应用https请求_Android_05 图5

如图5所示客户端、服务端都生成了三种数据:完整性加密秘钥mac key、数据加密秘钥encryption key、初始化向量IV
客户端使用Client的秘钥加密,发送给服务端,服务端再使用Client的秘钥进行解密
服务端使用Server的秘钥加密,返回给客户端,客户端再使用Server的秘钥进行解密

2.通过OpenSSL实践

2.1Linux环境安装OpenSSL

参考链接

其是通过下载openssl源码包,然后编译,再进行环境设置。
openssl官网进行下载源码包
OpenSSL的github链接

2.2创建自己的CA证书

生成根证书的秘钥
openssl genrsa -out ca-key.pem 2048


生成证书签发申请文件
openssl req -new -out ca-req.csr -key ca-key.pem


填入CA的相关信息
HTTPS自签名证书以及Android应用https请求_Android_06


生成根证书
openssl x509 -req -sha256 -in ca-req.csr -out ca-cert.pem -signkey ca-key.pem -days 3650


x509 签发X.509格式证书命令。
-req 表示证书输入请求。
-in 表示输入文件,这里为ca-req.csr,输入的文件是CSR, 那么就生成自签名文件
-out 表示输出文件,这里为ca-cer.pem。
-signkey 表示自签名密钥,这里为ca-key.pem。
-days 表示有效天数,这里为3650天。


将证书ca-cert.pem转换为pkcs格式
openssl pkcs12 -export -clcerts -in ca-cert.pem -inkey ca-key.pem -out ca.p12 -name ca


转换为pkcs格式,用于安装到浏览器中,作为客户端CA证书,这样在请求服务端证书后,才能识别服务端证书是该CA签名的,才会认为服务端证书有效,Export Password为导入浏览器时需要用的密码
HTTPS自签名证书以及Android应用https请求_Android_07


将证书ca-cert.pem转换为jks格式
需要在JDK环境下执行命令
keytool -keystore truststore.jks -keypass 123456 -storepass 123456 -alias ca -import -trustcacerts -file ca-cert.pem


truststore.jks是用来设置服务端的CA证书的,这样在双向认证的时候,服务端才能识别客户端证书是该CA签名的,才会认为客户端证书有效

HTTPS自签名证书以及Android应用https请求_Android_08

2.3创建服务端的证书

生成服务器端的密匙
openssl genrsa -out server-key.pem 2048


生成服务器端证书签发申请文件
openssl req -new -out server-req.csr -key server-key.pem


填入server相关信息,对应的Common Name要填写域名,测试可以填写字符串,不能填写ip地址
HTTPS自签名证书以及Android应用https请求_Android_09


给申请文件server-req.csr签名生成证书server-cert.pem
openssl x509 -req -sha256 -in server-req.csr -out server-cert.pem -signkey server-key.pem -CA ca-cert.pem -CAkey ca-key.pem -CAcreateserial -days 3650


x509 签发X.509格式证书命令。
-req 表示证书输入请求。
-in 表示输入文件,这里为server-req.csr,输入的文件是CSR, 那么就生成自签名文件
-out 表示输出文件,这里为server-cert.pem
-signkey 表示自签名密钥,这里为server-key.pem
-CA 表示CA证书,这里为ca-cert.pem
-CAkey 表示CA证书密钥,这里为ca-key.pem
-CAcreateserial表示创建CA证书序列号
-days 表示有效天数,这里为3650天。


将证书server-cert.pem转换为pkcs格式
openssl pkcs12 -export -clcerts -in server-cert.pem -inkey server-key.pem -out server.p12 -name server
pkcs12 PKCS#12编码格式证书命令。
-export 表示导出证书。
-clcerts 表示仅导出客户证书,不导出ca证书
-inkey 表示输入文件,这里为server-key.pem
-in 表示输入文件,这里为server-cert.pem,pem格式
-out 表示输出文件,这里为server.p12


转换为Tomcat配置能够识别的pkcs格式,Export Password为对应的密码
HTTPS自签名证书以及Android应用https请求_Android_10

2.4创建客户端的证书

生成客户端的密匙
openssl genrsa -out client-key.pem 2048


生成客户端证书签发申请文件
openssl req -new -out client-req.csr -key client-key.pem


填入client相关信息
HTTPS自签名证书以及Android应用https请求_Android_11


给申请文件client-req.csr签名生成证书client-cert.pem
openssl x509 -req -sha256 -in client-req.csr -out client-cert.pem -signkey client-key.pem -CA ca-cert.pem -CAkey ca-key.pem -CAcreateserial -days 3650


x509 签发X.509格式证书命令。
-req 表示证书输入请求。
-in 表示输入文件,这里为client-req.csr,输入的文件是CSR, 那么就生成自签名文件
-out 表示输出文件,这里为client-cert.pem
-signkey 表示自签名密钥,这里为client-key.pem
-CA 表示CA证书,这里为ca-cert.pem
-CAkey 表示CA证书密钥,这里为ca-key.pem
-CAcreateserial表示创建CA证书序列号
-days 表示有效天数,这里为3650天


将证书client-cert.pem转换为pkcs格式
openssl pkcs12 -export -clcerts -in client-cert.pem -inkey client-key.pem -out client.p12 -name client
pkcs12 PKCS#12编码格式证书命令。
-export 表示导出证书。
-clcerts 表示仅导出客户证书,不导出ca证书
-inkey 表示输入文件,这里为client-key.pem
-in 表示输入文件,这里为client-cert.pem,pem格式
-out 表示输出文件,这里为client.p12


转换为pkcs格式,用于安装到浏览器中,Export Password为导入浏览器时需要用的密码
HTTPS自签名证书以及Android应用https请求_Android_12

2.5服务端Tomcat部署https请求

进入Tomcat根目录,编辑”…/conf/server.xm”文件,新增Connector节点

<Connector port="8443" protocol="HTTP/1.1" SSLEnabled="true"  
           maxThreads="150" scheme="https" secure="true"  
           clientAuth="false" sslProtocol="TLS"  
           keystoreFile="/root/ssl/server.p12" keystorePass="123456"  
           truststoreFile="/root/ssl/truststore.jks" truststorePass="123456" 
          URIEncoding="utf-8"/>

port 443 表示SSL的默认端口号,也可以自定义端口号比如8443
clientAuth 表示是否需要双向验证,false为不需要,true表示需要
keystoreFile 表示服务端密钥库server.p12所在目录
keystorePass 表示服务端密钥库密码
truststoreFile 表示CA证书truststore.jks所在目录
truststorePass 表示CA证书密码 *

配置完成需要重新启动Tomcat

3测试

3.1OpenSSL命令测试

如果服务端https端口配置的443,则不用填写端口号,直接填写域名即可
openssl s_client -connect 你的域名:端口号 -cert client-cert.pem -certform PEM -key client-key.pem -tls1 -CAfile ca-cert.pem

3.2浏览器访问测试

3.2.1设置客户端电脑域名跳转

如果是在自己电脑服务器或者局域网内的服务器做测试,可以设置客户端电脑域名跳转 修改C:\Windows\System32\drivers\etc\hosts,在hosts文件末尾添加域名跳转IP的设置
10.218.5.10 www.test.server


HTTPS自签名证书以及Android应用https请求_Android_13

3.2.2安装CA证书

双击ca.p12文件
下一步
下一步
输入其上面对应的Export Password,下一步
选择"将所有的证书放入下列存储",点击浏览,选择"受信任的根证书颁发机构",下一步
完成

3.2.3安装客户端证书

双击client.p12文件
下一步
下一步
输入其上面对应的Export Password,下一步
选择"根据整数类型,自动选择证书存储",下一步
完成

3.2.4浏览器访问

输入网址"https://www.test.server:8443"


在IE中正确访问,由于是自己充当的CA,不能得到浏览器的信任,但是这不妨碍https协议的认证,只要别人没有得到我们的CA私钥,则证书认证就可以信任。
HTTPS自签名证书以及Android应用https请求_Android_14

3.3Android App访问测试

3.3.1设置域名跳转

如果是测试域名,则需要设置域名跳转,我使用的Genymotion模拟器
在终端中输入:
adb root
adb remount
adb shell
echo “10.218.5.10” www.test.server > /system/etc/hosts


HTTPS自签名证书以及Android应用https请求_Android_15

3.3.2编写APP

3.3.2.1添加权限

<uses-permission android:name="android.permission.INTERNET"/>

3.3.2.2放入证书

在res目录下新建raw
将ca-cert.pem、server-cert.pem、client.p12
将名称修改为ca_cert.pem、server_cert.pem,不然不能识别为资源文件

3.3.2.3编写代码

package chief.river.zxl.com.test_https_3;import android.app.Activity;import android.os.Bundle;import java.io.IOException;import java.io.InputStream;import java.net.URL;import java.security.KeyManagementException;import java.security.KeyStore;import java.security.KeyStoreException;import java.security.NoSuchAlgorithmException;import java.security.SecureRandom;import java.security.UnrecoverableKeyException;import java.security.cert.Certificate;import java.security.cert.CertificateException;import java.security.cert.CertificateFactory;import javax.net.ssl.HostnameVerifier;import javax.net.ssl.HttpsURLConnection;import javax.net.ssl.KeyManagerFactory;import javax.net.ssl.SSLContext;import javax.net.ssl.SSLSession;import javax.net.ssl.TrustManagerFactory;public class MainActivity extends Activity {

    private String mUrlStr = "https://www.test.server:8443";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        doHttpsRequest();
    }

    private void doHttpsRequest(){
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    SSLContext mSslContext = getSslContext();

                    URL mUrl = new URL(mUrlStr);

                    HttpsURLConnection mHttpsURLConnection = (HttpsURLConnection) mUrl.openConnection();
                    mHttpsURLConnection.setSSLSocketFactory(mSslContext.getSocketFactory());
                    mHttpsURLConnection.setHostnameVerifier(new HostnameVerifier() {
                        @Override
                        public boolean verify(String hostname, SSLSession session) {
                            boolean result = HttpsURLConnection.getDefaultHostnameVerifier().verify(hostname,session);
                            return result;
                        }
                    });

                    mHttpsURLConnection.connect();

                    InputStream is = mHttpsURLConnection.getInputStream();
                    byte[] buffer = new byte[1024];
                    StringBuilder mStringBuilder = new StringBuilder();
                    while ((is.read(buffer,0,buffer.length)) != -1){
                        mStringBuilder.append(new String(buffer));
                    }
                    System.out.println("zxl--->mStringBuilder--->"+mStringBuilder);
                } catch (KeyStoreException e) {
                    e.printStackTrace();
                } catch (NoSuchAlgorithmException e) {
                    e.printStackTrace();
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (KeyManagementException e) {
                    e.printStackTrace();
                } catch (CertificateException e) {
                    e.printStackTrace();
                } catch (UnrecoverableKeyException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    private SSLContext getSslContext() throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException, KeyManagementException, UnrecoverableKeyException {
        //导入根证书
        //如果是Android7.0版本,系统不能再添加我们自定义的CA证书了
        //因此这边需要使用server_cert.pem,直接信任服务器证书就可以了,当然7.0之前版本用服务器证书也可以
        InputStream mCaInputStream = getResources().openRawResource(R.raw.ca_cert);
        CertificateFactory mCertificateFactory = CertificateFactory.getInstance("X509");
        Certificate mCertificate = mCertificateFactory.generateCertificate(mCaInputStream);

        if(mCaInputStream != null){
            mCaInputStream.close();
        }

        //生成根证书对应的秘钥库
        KeyStore mCaKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
        mCaKeyStore.load(null,null);
        mCaKeyStore.setCertificateEntry("ca",mCertificate);

        //初始信任管理
        TrustManagerFactory mTrustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        mTrustManagerFactory.init(mCaKeyStore);

        //生成客户端证书秘钥库
        InputStream mClientInputStream = getResources().openRawResource(R.raw.client);
        KeyStore mClientKeyStore = KeyStore.getInstance("PKCS12");
        mClientKeyStore.load(mClientInputStream,"123456".toCharArray());

        if(mClientInputStream != null){
            mClientInputStream.close();
        }

        //初始化证书秘钥库
        KeyManagerFactory mKeyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        mKeyManagerFactory.init(mClientKeyStore,"123456".toCharArray());



        SSLContext mSslContext = SSLContext.getInstance("TLS");
        //如果是双向认证则需要mKeyManagerFactory.getKeyManagers(),否则为null
        mSslContext.init(mKeyManagerFactory.getKeyManagers(),mTrustManagerFactory.getTrustManagers(),null);
        return mSslContext;

    }}

3.3.3Android7.0进行HTTPS访问

3.3.3.1HTTPS访问异常

因为7.0以后系统不能再添加自定义的CA证书了,所以如果还是添加自己的CA证书,则会报错

04-03 17:37:31.705 1682-1697/? W/System.err: Caused by: java.security.cert.CertificateException: Chain validation failed04-03 17:37:31.705 1682-1697/? W/System.err:     at com.android.org.conscrypt.TrustManagerImpl.verifyChain(TrustManagerImpl.java:610)04-03 17:37:31.705 1682-1697/? W/System.err:     at com.android.org.conscrypt.TrustManagerImpl.checkTrustedRecursive(TrustManagerImpl.java:444)04-03 17:37:31.705 1682-1697/? W/System.err:     at com.android.org.conscrypt.TrustManagerImpl.checkTrustedRecursive(TrustManagerImpl.java:464)04-03 17:37:31.705 1682-1697/? W/System.err:     at com.android.org.conscrypt.TrustManagerImpl.checkTrusted(TrustManagerImpl.java:401)04-03 17:37:31.705 1682-1697/? W/System.err:     at com.android.org.conscrypt.TrustManagerImpl.checkTrusted(TrustManagerImpl.java:375)04-03 17:37:31.705 1682-1697/? W/System.err:     at com.android.org.conscrypt.TrustManagerImpl.getTrustedChainForServer(TrustManagerImpl.java:304)04-03 17:37:31.705 1682-1697/? W/System.err:     at android.security.net.config.NetworkSecurityTrustManager.checkServerTrusted(NetworkSecurityTrustManager.java:94)04-03 17:37:31.705 1682-1697/? W/System.err:     at android.security.net.config.RootTrustManager.checkServerTrusted(RootTrustManager.java:88)04-03 17:37:31.705 1682-1697/? W/System.err:     at com.android.org.conscrypt.Platform.checkServerTrusted(Platform.java:178)04-03 17:37:31.706 1682-1697/? W/System.err:     at com.android.org.conscrypt.OpenSSLSocketImpl.verifyCertificateChain(OpenSSLSocketImpl.java:596)04-03 17:37:31.706 1682-1697/? W/System.err:     at com.android.org.conscrypt.NativeCrypto.SSL_do_handshake(Native Method)04-03 17:37:31.706 1682-1697/? W/System.err:     at com.android.org.conscrypt.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:357)04-03 17:37:31.706 1682-1697/? W/System.err: 	... 13 more04-03 17:37:31.706 1682-1697/? W/System.err: Caused by: java.security.cert.CertPathValidatorException: Path does not chain with any of the trust anchors04-03 17:37:31.706 1682-1697/? W/System.err:     at sun.security.provider.certpath.PKIXCertPathValidator.validate(PKIXCertPathValidator.java:156)04-03 17:37:31.706 1682-1697/? W/System.err:     at sun.security.provider.certpath.PKIXCertPathValidator.engineValidate(PKIXCertPathValidator.java:79)04-03 17:37:31.706 1682-1697/? W/System.err:     at java.security.cert.CertPathValidator.validate(CertPathValidator.java:301)04-03 17:37:31.706 1682-1697/? W/System.err:     at com.android.org.conscrypt.TrustManagerImpl.verifyChain(TrustManagerImpl.java:606)04-03 17:37:31.706 1682-1697/? W/System.err: 	... 24 more

3.3.3.2解决异常

方法1:
可以根据3.3.2.3编写代码中,替换为信任服务端证书server_cert.pem来解决

//导入根证书
//如果是Android7.0版本,系统不能再添加我们自定义的CA证书了
//因此这边需要使用server_cert.pem,直接信任服务器证书就可以了,当然7.0之前版本用服务器证书也可以

InputStream mCaInputStream = getResources().openRawResource(R.raw.server_cert);CertificateFactory mCertificateFactory = CertificateFactory.getInstance("X509");Certificate mCertificate = mCertificateFactory.generateCertificate(mCaInputStream);


方法2:
参考Android官网指导文档,通过配置xml来设置信任服务端证书server_cert.pem来解决


在res目录下新建xml文件夹,在xml下创建文件network_security_config.xml
信任附加证书
HTTPS自签名证书以及Android应用https请求_Android_16
或者限制可信证书集,信任的证书和对应的服务端地址相对应,不对应则身份认证失败
HTTPS自签名证书以及Android应用https请求_Android_17
在AndroidManifest.xml的application标签中添加network_security_config
HTTPS自签名证书以及Android应用https请求_Android_18
修改3.3.2.3编写代码中的getSslContext方法,去除掉导入信任库的部分就可以了
HTTPS自签名证书以及Android应用https请求_Android_19