android https访问
https的握手过程
https在传输真正的数据之前,需要客户端和服务端进行一次协议握手,主要配置好两边的私钥和一些初始化工作,大致流程如下图:
注意区分第2步骤的证书个公钥,证书由专门的第三方证书机构颁发,一个证书生成后,包含有申请者的机构信息、组织机构、证书有效期和一串hash签名,除此之外还有证书自己的公私钥;上诉第二个步骤中的公钥是服务器的公钥,不要和证书的公钥混淆
使用中遇到的问题
最近项目需要使用https方式访问,项目是用retrofit+okhttp框架,需要把以前的http全部改为https访问;关于https访问配置的方式有三种
- 信任所有证书
- 验证某一个特定证书
信任所有证书
直接将以前的http改成https即可,不需要做任何改变,照理说第一种改变https即可,但是我却出现这种异常
Suppressed: javax.net.ssl.SSLHandshakeException: Handshake failed
04-17 18:09:07.892 3160-3160/com.chinamobile.iot.easiercharger W/System.err: ... 36 more
04-17 18:09:07.892 3160-3160/com.chinamobile.iot.easiercharger W/System.err: Caused by: javax.net.ssl.SSLProtocolException: SSL handshake aborted: ssl=0x9e23da80: Failure in SSL library, usually a protocol error
04-17 18:09:07.892 3160-3160/com.chinamobile.iot.easiercharger W/System.err: error:100bd10c:SSL routines:ssl3_get_record:WRONG_VERSION_NUMBER (external/boringssl/src/ssl/s3_pkt.c:311 0xa92997f7:0x00000000)
04-17 18:09:07.902 3160-3160/com.chinamobile.iot.easiercharger W/System.err: at com.android.org.conscrypt.NativeCrypto.SSL_do_handshake(Native Method)
04-17 18:09:07.902 3160-3160/com.chinamobile.iot.easiercharger W/System.err: at com.android.org.conscrypt.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:353)
04-17 18:09:07.902 3160-3160/com.chinamobile.iot.easiercharger W/System.err: ... 35 more
04-17 18:09:07.902 3160-3160/com.chinamobile.iot.easiercharger W/System.err: Caused by: javax.net.ssl.SSLProtocolException: SSL handshake aborted: ssl=0x9e23da80: Failure in SSL library, usually a protocol error
04-17 18:09:07.902 3160-3160/com.chinamobile.iot.easiercharger W/System.err: error:100bd10c:SSL routines:ssl3_get_record:WRONG_VERSION_NUMBER (external/boringssl/src/ssl/s3_pkt.c:311 0xa92997f7:0x00000000)
04-17 18:09:07.902 3160-3160/com.chinamobile.iot.easiercharger W/System.err: at com.android.org.conscrypt.NativeCrypto.SSL_do_handshake(Native Method)
04-17 18:09:07.902 3160-3160/com.chinamobile.iot.easiercharger W/System.err: at com.android.org.conscrypt.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:353)
04-17 18:09:07.902 3160-3160/com.chinamobile.iot.easiercharger W/System.err: ... 35 more
这种问题原因有多重,可能证书有问题,也可能是自己的代码有问题;我的项目出现这个问题的原因是:
未改变https之前,我的网络接口类似于这种
http://www.power.com:80
改成https后是这种
https://www.power.com:80
改了之后就出现上诉的错误,你们能看出问题所在吗?
那就是端口号
http协议默认使用的端口号是80,https默认的端口是443,而这里我改动https后端口号没变,所以这个域名是访问不通的,除非https通信端口号被改成了80才会通,解决办法就是端口号改成443即可解决
这是我使用第一种方式引发的问题,如果使用自签名的证书或者忽略所有证书,一旦你的端口号有误,也可能出现上诉问题,伙伴们,擦亮眼睛吧!
信任某一特定证书
获取证书
可以从网站上导出证书,具体方法可点击参考,导出比较简单,重要的是格式,博主在导出的.cer格式的证书后,反正asstes目录下,最后以stream方式打开,但是在okhttp使用这个证书文件验证时总出错,报错信息大致是code编码问题,思考了良久和查询了资料,最后确定原因是证书用在okhttp框架里去的时候要永城utf-8格式,以下是解决方案:
- 将cer文件里面的key原封不等的拷贝出来
public final static String SSL_KEY = "-----BEGIN CERTIFICATE-----\n" +
"Fw0xODAxMTYwMDAwMDBaFw0yMDAyMTUxMjAwMDBaMIGdMQswCQYDVQQGEwJDTjEQ\n" +
"MA4GA1UEBxMHQ2hlbmdkdTE4MDYGA1UEChMvQ2hpbmEgTW9iaWxlIElPVCBDb21w\n" +
"YW55IExpbWl0ZWQgQ2hlbmdkdSBicmFuY2gxJjAkBgNVBAsTHUluZm9ybWF0aW9u\n" +
"IFRlY2hub2xvZ3kgQ2VudGVyMRowGAYDVQQDExF3d3cudGF4aWFpZGVzLmNvbTCC\n" +
"ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOryUl/OGkrUkqSzoimarOPv\n" +
"V0qQ7EsyS1ny0UZ7jcxnFS7ztOLh/0XIaPvX4e2KWgdcgxL8LbQ/gFKeRr5s6Uub\n" +
"QUeczE9+CO4ic5opzS76QVJVH0kTSBoB1HBJ0TAV3XhSt+SOF7T5bpJrcCdijw7X\n" +
"-----END CERTIFICATE-----";
- 导入时要进行一次utf编码
InputStream[] key = new InputStream[]{new Buffer().writeUtf8(SSL_KEY_TEST).inputStream()};
- 进行证书验证
public static SSLParams setCertificates(InputStream... certificates)
{
SSLParams sslParams = new SSLParams();
try
{
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null);
int index = 0;
for (InputStream certificate : certificates)
{
String certificateAlias = Integer.toString(index++);
keyStore.setCertificateEntry(certificateAlias, certificateFactory.generateCertificate(certificate));
try
{
if (certificate != null){
certificate.close();
}
} catch (IOException e)
{
e.printStackTrace();
}
}
SSLContext sslContext = SSLContext.getInstance("TLS");
TrustManagerFactory trustManagerFactory =
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(keyStore);
sslContext.init
(
null,
trustManagerFactory.getTrustManagers(),
new SecureRandom()
);
sslParams.trustManager = (X509TrustManager) trustManagerFactory.getTrustManagers()[0];
sslParams.sSLSocketFactory = sslContext.getSocketFactory();
return sslParams;
} catch (Exception e)
{
e.printStackTrace();
}
return sslParams;
}
成功后,可以去换一个错误的证书测试一下,不要尝试着去修改正确证书里面的内容,这样会造成第三步骤里面证书验证抛出异常,导致所有证书这块代码都没执行,信任所有证书;建议去其他https网站,按照上面步骤导出一个第三方的cer证书,最后用错误的证书测试会有以下几个字眼,就表示成功;
Trust anchor for certification path not found.
服务端验证
以上讲述的都是客户端验证服务端的证书,客户端只包含了服务端的公钥;而双向验证则会多了几点,详细请看下:
- 客户端发送自己的SSL版本信息、支持加密算法等信息
- 服务端收到后发送SSL版本信息、支持加密算法等信息,并且发送自己的公钥证书给客户端
- 客户端会验证公钥证书的合法性,CA机构颁布、过期或者是否信任
- 如果不通过将会终止
- 服务端会要求客户端发送客户端自己的公钥证书给服务端,收到后,服务端会对其证书进行验证
- 验证通过后,会获得客户端的公钥;至此,客户端有自己的私钥和服务端的公钥;服务端有自己的私钥和客户端的公钥
- 客户端会发送自己所支持的对称加密方案,服务端会选择一个加密程度最高的算法
- 选择后,会用客户端的公钥加密选择的方案,加密后发送给客户端
- 客户端用自己的私钥进行解密,并使用该加密方案产生初始的加密秘钥
- 客户端使用服务端的公钥加密该对称私钥,发送给服务端
- 服务端收到加密对称私钥后,用自己的私钥解密后会获得对称秘钥,
- 最后,两边都获得了对称秘钥,使用该秘钥进行通信
单向认证没有服务端对客户端的证书验证,并且在第8步骤是明文发送加密方案
代码完成
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
//读取客户端公钥
InputStream ksIn = context.getAssets().open(CLIENT_PRI_KEY);
//keystore加载秘钥和密码
keyStore.load(ksIn, CLIENT_BKS_PASSWORD.toCharArray());
ksIn.close();
//初始化SSLContext
SSLContext sslContext = SSLContext.getInstance(“TLS”);
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("X.509");
keyManagerFactory.init(keyStore, CLIENT_BKS_PASSWORD.toCharArray());
sslContext.init(keyManagerFactory.getKeyManagers(), null, null);
sslSocketFactory = sslContext.getSocketFactory();
证书验证
- 获取证书管理类CertificateFactory和KeyStore
- 使用证书管理类CertificateFactory和证书初始化Keystore
- 获取证书信任管理类TrustManagerFactory,将Keystore里面的证书添加到证书信任管理类中
- SSL上下文类SSLContext用证书信任管理类进行初始化
- 将SSL添加到okhttpbuilder里面即可
HTTPS如何抵御中间人
所谓中间人,也就是在https握手阶段,分配对称秘钥时,对服务端,将自己伪装成客户端,对客户端伪装成服务端,使他们相互无感知,从而窃取、修改他们传递的内容。
详细来说,就是第二步,如果服务端将自己公钥以明文方式发送给客户端,被中间人拦截后,保存下来,自己在生成一对非对称加密公私钥,将自己的公钥发送给客户端,这样客户端完全不知道拿到的公钥不是服务端,客户端在用公钥加密自己对称加密的私钥,发送到服务端,再次被拦截,中间人用自己的私钥解密就拿到了客户端发送的私钥,这样就毫无安全可言了!
所以,问题的根本,如何保证收到的服务端的公钥确定是正确的,来自服务端的?
CA证书
CA证书由专门的机构颁发,这些是公开可信任的!服务端在部署HTTPS前,需要将自己的信息提供的CA证书机构,其中包括服务端公钥,域名,证书有效期,组织机构,地域,证书序列号,颁发机构等信息。这些信息正文,通过散列算法H,形成信息摘要,CA对生成的信息摘要,通过CA自身的非对称加密的私钥进行加密,得到密文,此密文称之为数字签名(CA对信息摘要进行了签名),信息正文、数字签名,放在一起,形成数字证书。
对于这些公开可信任机构颁发的证书,系统一般都集成了这些证书,也就是说客户端提前就存储了CA证书的公钥;证书验证的步骤
-
客户端向服务端发起HTTPS请求
-
服务端发送将证书信息正文和证书加密后的数字签名
数字签名,是证书的信息正文hash散列值后,再用CA的私钥加密后的签名
-
客户端拿到证书后,与系统中集成或者提前保存的CA信息进行对比,发现又相同的CA信息,就用该CA信息中心的公钥进行解密,获得到证书的信息正文的hash值;在将证书信息正文hash一次,与解密得到的hash对比,相等就说明是对的,反之则说明被串改了
中间人拦截到证书也知道CA公钥怎么办?
此时,若中间人拦截拿到证书后,也知道CA公钥的前提下,就算中间人修改证书内容还是替换证书内容,客户端拿到篡改的证书后,
一是解密用到的CA公钥是没办法解密中间人的东西,这是走不通的,因为CA公钥只能解密CA私钥加密的东西,而CA私钥中间人拿不到,服务端保存的
二是就算客户端用CA公钥解密成功后,他会发现hash值和明文的hash值是不等的,因为它篡改了内容
中间人篡改整个证书
假如A服务端申请了一个CA证书A,B也去弄一个CA机构颁发的证书B,然后B拦截A,将证书替换为自己的B证书,客户端拿到证书后怎么办?
例如,抓包工具Charles就是利用这一点,在抓取Https包时,我们一般客户端会先按照Charles的证书并且信任,这样Charles抓取到包后,使用自己的证书发送给客户端,客户端如果信任所有的证书情况下,是无感知的,相当于中间人完美拦截!
当然,解决办法就是不信任呗!不要手贱去安装一些第三方的证书,说不定什么时候就被攻击了!
如果不小心点了怎么办,一种办法是向本文前面那要,客户端固定某一个证书,解密后与客户端自己保存的对比,这样就解决了!
其次,证书机构颁发的一般都有网站信息,这点很简单一比就会发现不适自己的网站!校验域名!