老牛知点所以然-两种安卓两种网络请求框架(ksoap2-android & okhttp)配置https

什么是HTTPS

关于https与http的区别,耳朵都快被磨出茧子了,但是除了知道这玩意比http更安全之外,SSL握手阶段,到底发生了什么,也说不出一个具体的所以然来。囫囵了几篇关于安卓客户端不同网络请求框架怎么配置HTTPS的文章,大都是一通Ctrl C + Ctrl V,一通操作,却不讲所以然。尤其是X509那一段代码,不明白为什么checkClientTrustedcheckServerTrusted两个抽象方法,根本不用实现,代码就能跑起来;不明白,网文上getAcceptedIssuers这个方法,有的返回一个null,有的直接返回一个构建的空数组,就能达到信任所有证书的效果(这个现在也没明白,没有看到相应源码);也不明白,为什么说信任所有证书是一个糟糕的主意。看来看去,反而却更加蒙圈了。
最近得闲,看了几篇博文,终于大概知道个全貌,由于大都是自己的理解,其中会有谬误,欢迎指正交流。。
以下是文章列表:

  1. 安卓开发者网站关于HTTPS的介绍
  2. JSSE
  3. X509证书信任管理器类的详解

要说文档写的严谨科学的,还是我大谷歌的大安卓开发者网站,字字珠玑,没有一点废话,惹得我赶紧保存pdf以防以后找不到了。以下这段话整段复制自安卓开发者网站:

在典型的 SSL 使用场景中,服务器会配置有一个证书,该证书包含一个公钥及一个匹配的私钥。作为 SSL 客户端与服务器握手的一部分,服务器将通过使用公钥加密对其证书进行签名来证明自己具有私钥。
不过,任何人都可以生成他们自己的证书和私钥,因此,一个简单的握手只能说明服务器知道与证书公钥匹配的私钥,除此之外无法证明服务器任何其他方面的信息。解决此问题的一个方法是让客户端拥有其信任的一个或多个证书集。如果证书不在此集合中,则不会信任服务器。
这种方法虽然简单,但有几个缺点。服务器应能够随时间的推移升级到更强的密钥(“密钥轮换”),使用新的公钥替换证书中的公钥。遗憾的是,客户端应用现在必须根据服务器配置发生的变化进行更新。如果服务器不在应用开发者的控制下(例如,如果服务器是一个第三方网络服务),则很容易出现问题。如果应用必须与网络浏览器或电子邮件应用等任意服务器通信,那么,此方法也会带来问题。
为弥补这些缺点,通常使用知名颁发机构(称为证书授权机构 (CA))颁发的证书配置服务器。主机平台一般包含其信任的知名 CA 的列表。从 Android 4.2 (Jelly Bean) 开始,Android 目前包含在每个版本中更新的 100 多个 CA。CA 具有一个证书和一个私钥,这点与服务器相似。为服务器颁发证书时,CA 使用其私钥为服务器证书签名。然后,客户端可以验证该服务器是否具有平台已知的 CA 颁发的证书。
不过,在解决一些问题的同时,使用 CA 也会引发其他问题。因为 CA 为很多服务器颁发证书,所以您仍需要某种方式来确保您与您需要的服务器通信。为解决此问题,CA 颁发的证书通过 gmail.com 等具体名称或 *.google.com 等通配型主机集识别服务器。

摘录出来的这几句话,明确了如下几个问题:

  1. 非对称加密,是https的基础。服务端,握有一个秘钥对,在单向验证的情况下,客户端服务端完成client hello 和 server hello(也就是二者商定完加密算法和秘钥长度)之后,服务端会向客户端提供自己秘钥对中的公钥,在建立完安全链接之后,客户端向服务端发送的业务数据,都是通过这个公钥加密的。而服务端向客户端发送的业务数据,都是通过服务端的私钥加密的。在双向验证的情况下,server端向客户端提供公钥后,还会顺带手,向客户端发送一个证书请求,意思是:哥们,我公钥给你了,你的公钥也发过来让我瞅瞅吧!而如果客户端收到了这个请求,则会在自己的ssl握手阶段,向服务端提供自己的公钥,让服务端去验证。注意:我对这一块知识感到蒙圈,很大一部分原因,是因为我在这里已经弄不清楚,公钥秘钥和证书的界限了。因为谈及加密解密,用的都是公钥私钥,说着说着公钥私钥,突然就说起了证书。其实,整个握手阶段,客户端与服务端交换的公钥,都是以证书的形式交换的。所以应该这样理解:证书,是公钥秘钥的载体,是对公钥秘钥的一种形式上的包装,但是,实际上,私钥是不可能被交换的,交换的只是公钥,所以更准确的理解是:证书,是公钥的一种包装方式,以证书为载体,交换公钥(单向验证时,不存在交换,只是服务端,向客服端提供带有公钥信息的证书)。
  2. 啥是CA,为什么会有CA?安卓开发者网站说的明白,CA的出现,是为了契合秘钥轮换的需求。注意:CA这个简写,是导致我对这块知识蒙圈的另外一大诱因,这里的误解,主要原因是上下文不明,导致对机构和证书两个词语边界理解模糊。正确的语境应该是:客户端接收到服务端发来的证书,会检查一下,这个证书,是不是一个受信任的CA签发的。所谓受信任的CA,可以理解为“话事人”或者“带头大哥”,如果是话事人或者带头大哥给担保了的,则服务器发过来的证书,就是可以信任的。也就是CA的出现,一方面是解决证书轮换的需求,另一方面,是防止客户端被“诱骗”的一个重要手段。每一个授信的CA,会给很多个服务器提供的公钥(还是证书?),提供证书签名服务(做担保)。客户端想跟一个服务端做SSL通信,发现服务器提供的证书,不是授信的CA签发的,就会终止此次通信。详见JSSEPublic Key Certificates部分。也就是说,受信任的CA,所担保的证书,是以一个列表的形式存在的。当客户端与服务端,使用自签名的证书,而客户端不做任何处理时,由于自签名的证书,是自己生成的,而不是话事人担保的,客户端在检查服务端发送过来的证书时,没有在话事人担保列表里找到担保过的证书,于是就会javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.

为了解决什么问题

以下,复制自JSSE:

You cannot always be sure that the entity with whom you are communicating is really who you think it is.
Network data can be intercepted, so it is possible that it can be read by an unauthorized third party, sometimes known as an attacker.
If an attacker can intercept the data, the attacker may be able to modify the data before sending it on to the receiver.

SSL解决的核心的三个问题:

  1. 防止客户端被诱骗
  2. 防止数据被嗅探
  3. 防止数据被篡改

SSL握手细节

JSSE中SSL握手步骤
这每一步,JSSE都做了详尽的说明:

  1. Client hello - The client sends the server information including the highest version of SSL it supports and a list of the cipher suites it supports. (TLS 1.0 is indicated as SSL 3.1.) The cipher suite information includes cryptographic algorithms and key sizes.
  2. Server hello - The server chooses the highest version of SSL and the best cipher suite that both the client and server support and sends this information to the client.
  3. Certificate - The server sends the client a certificate or a certificate chain. A certificate chain typically begins with the server’s public key certificate and ends with the certificate authority’s root certificate. This message is optional, but is used whenever server authentication is required.
  4. Certificate request - If the server needs to authenticate the client, it sends the client a certificate request. In Internet applications, this message is rarely sent.
  5. Server key exchange - The server sends the client a server key exchange message when the public key information sent in message 3 above is not sufficient for key exchange. For example, in ciphersuites based on Diffie-Hellman, this message contains the server’s DH public key.
  6. Server hello done - The server tells the client that it is finished with its initial negotiation messages.
  7. Certificate - If the server requests a certificate from the client in message 4, the client sends its certificate chain, just as the server did in message 3.
    Note: Only a few Internet server applications ask for a certificate from the client.
  8. Client key exchange - The client generates information used to create a key to use for symmetric encryption. For RSA, the client then encrypts this key information with the server’s public key and sends it to the server. For ciphersuites based on Diffie-Hellman, this message contains the client’s DH public key.
  9. Certificate verify - This message is sent when a client presents a certificate as previously explained. Its purpose is to allow the server to complete the process of authenticating the client. When this message is used, the client sends information that it digitally signs using a cryptographic hash function. When the server decrypts this information with the client’s public key, the server is able to authenticate the client.
  10. Change cipher spec - The client sends a message telling the server to change to encrypted mode.
  11. Finished - The client tells the server that it is ready for secure data communication to begin.
  12. Change cipher spec - The server sends a message telling the client to change to encrypted mode.
  13. Finished - The server tells the client that it is ready for secure data communication to begin. This is the end of the SSL handshake.
  14. Encrypted data - The client and the server communicate using the symmetric encryption algorithm and the cryptographic hash function negotiated in messages 1 and 2, and using the secret key that the client sent to the server in Message 8. The handshake can be renegotiated at this time. See the next section for details.
  15. Close Messages - At the end of the connection, each side will send a close_notify message to inform the peer that the connection is closed.

其中,步骤三是可选的,有点不理解,不知道为什么,按照我的理解,hppts通信,在步骤三,将自己的证书,提供给客户端是必不可少的,不知道这里为什么是可选。

我的理解:潘名媛西门富帅与HTTPS

到这里,突然脑海里想起里一个不太贴合所有实际,但是能让SSL握手过程显得略微那么具体一点的比喻:潘名媛与西门富帅
假设,潘名媛是服务器,西门富帅是客户端
在http阶段,西门富帅月黑风高,去到潘名媛的家里,直接推门而入,方便是方便,但是缺点在于,知道潘名媛ip地址的歪瓜裂枣们都可以推门而入。
于是,为了堵住这个漏洞,潘名媛在门上安装了一个密码锁,二人约定:

  1. 当听到(server hello)门外响起熙熙碎碎的脚步声(client hello)
  2. 潘名媛告诉门外人锁的密码(证书,公钥)
  3. 西门富帅根据公钥,打开密码锁,进入庭院,潘名媛再根据进来的是谁,决定让他滚出去还是其他。

但是这样做有个隐患:潘名媛有个女邻居,垂涎西门富帅已久,且西门富帅晚上眼神不太好,于是女邻居也在门上装了一把密码锁。等晚上西门富帅往潘名媛家门口去的时候,她抢先报出了自己门上的密码,西门官人不明所以(信任了所有证书),推开邻居家门进去了,牺牲了色相和钱财。

为了堵住这个隐患,二人商量了一个对策:请王妈妈出场(受信任的CA)。潘名媛将自己的公钥,存在了王妈妈那里,当西门官人走到邻居门前时,听到了里面报的公钥,没着急解锁进门,而是从王妈妈那里先查了一下,这个公钥,在王妈妈那里报备了没。结果发现这个没报备,于是大骂了一声(SSLHandshakeException),继续往前走。

但是假如潘西二人,开始业务时,王妈妈业务已经很多了,佣金也很高,对客户资质要求也非常严,他们这单生意人家根本看不上,但是为了保护潘宝宝不受诱骗,他们想了一个主意:潘名媛给西门富帅做了一个锦囊(TrustManager),里面盛的是自家密码锁公钥合法性证书文件(自签名)。于是西门富帅走夜路时,每听到有人报公钥之后,都拿出锦囊来,查验一下,这个公钥是潘名媛的吗,从此,就再也不受诱骗了。

为什么说信任所有证书是一个糟糕的主意

客户度,默认只信任受信任CA签发的证书。安卓开发者网站上提到的,那个糟糕的,信任所有证书的方案,实际上,是让客户端,变成了一个“小傻子”,即,不管和你SSL握手的服务端是什么角色,也不管它是自家的,还是歹人的,自己都掏心掏肺的和它进行通讯。参照以上提到的西门富帅信任所有证书的窘境。

来自人工只能Kimi对上述比喻的评价

总的来说,这个比喻在传达HTTPS通信的基本思想上是有帮助的,但在技术细节上有所简化和偏差。实际的HTTPS通信过程更为复杂,涉及多个步骤和安全机制,以确保数据传输的安全性和可靠性。

  • 客户端和服务器的身份验证:

    比喻中提到潘名媛(服务器)告诉西门富帅(客户端)密码(公钥),这实际上是不准确的。在HTTPS中,服务器会向客户端提供一个数字证书,这个证书包含了服务器的公钥,并且由一个受信任的证书颁发机构(CA)签发。客户端使用这个公钥来加密一个随机生成的对称密钥,然后将加密后的密钥发送给服务器。服务器使用自己的私钥解密这个对称密钥,之后双方就可以使用这个对称密钥来加密通信数据。

  • SSL握手过程:

    比喻中没有提到SSL握手过程中的多个步骤,如服务器的证书验证、密钥交换、会话密钥的生成等。在实际的SSL握手过程中,客户端会验证服务器的证书是否由受信任的CA签发,以及证书是否在有效期内等信息。如果证书验证通过,客户端会生成一个随机数(预主密钥),并使用服务器的公钥加密后发送给服务器。服务器使用私钥解密得到预主密钥,然后双方使用预主密钥生成会话密钥,用于后续的加密通信。

来自Kimi的补充完善:

让我们在保持原有比喻的基础上,对其进行扩展和细化,以便更准确地反映HTTPS的工作原理。

1. 客户端的初始化(西门富帅的准备)

西门富帅(客户端)在前往潘名媛(服务器)家之前,已经安装了一个安全的系统(安全协议),这个系统内置了一个信任的列表(受信任的CA列表),里面记录了所有值得信赖的“王妈妈”(证书颁发机构)的信息。这样,西门富帅在到达目的地之前,就已经具备了辨别真伪的能力。

2. SSL握手开始(敲门与确认)

西门富帅到达潘名媛家门口,他首先敲门(发送ClientHello消息),告知潘名媛他来访了,并且提供了一份自己能够理解的加密算法列表(cipher suites)。

3. 服务器响应(潘名媛的确认)

潘名媛听到敲门声后,从门缝里查看确认是西门富帅,并通过门缝递给他一个自己的名片(发送ServerHello消息),名片上包含了她的公钥信息,并且告诉西门富帅她选择了双方都能懂的加密算法。

4. 证书验证(核实身份)

西门富帅拿到名片后,他会检查名片上的信息是否由他信任的“王妈妈”(CA)签发的。如果是,那么他就相信这个公钥是真实可靠的,并且开始使用这个公钥来加密一个随机生成的密码(预主密钥),然后将其传回给潘名媛。

5. 密钥交换(交换密码)

潘名媛收到加密的密码后,使用自己的私钥解密得到预主密钥。此时,双方都有了相同的预主密钥,但这个密钥是临时生成的,仅用于本次会话,以保证安全性。

6. 完成握手(确认无误)

接下来,双方使用预主密钥生成会话密钥,并用会话密钥加密一段信息发送给对方,以确认彼此都掌握了正确的会话密钥。这个过程称为Finished消息交换。一旦确认无误,SSL握手过程完成,双方开始使用会话密钥加密通信。

7. 数据传输(安全对话)

现在,西门富帅和潘名媛可以使用他们共同拥有的会话密钥来加密和解密他们之间的对话。即使有人窃听,他们也无法理解加密后的内容,因为只有西门富帅和潘名媛知道会话密钥。

如何加载自签名证书

其实这块代码比较陌生。总体流程是:

  1. 通过KeyStore类,加载证书文件
  2. 通过加载完证书的KeyStore实例,初始化TrustManagerFactory
  3. 通过TrustManagerFactory实例及其方法调用,初始化SSLContext
  4. 通过SSLContext实例,得到SSLSocketFactory
    具体代码实现(HttpsTransportSE部分是为了兼容项目里一些老的ksoap做的网络请求):
public class SSLHelper {

    private static final SSLHelper instance = new SSLHelper();
    private SSLSocketFactory sslSocketFactory;
    private X509TrustManager trustManager;
    private HostnameVerifier hostnameVerifier;

    private SSLHelper() {

        hostnameVerifier = new HostnameVerifier() {
            @Override
            public boolean verify(String hostname, SSLSession session) {
                return true;
            }
        };

        TrustManagerFactory trustManagerFactory = null;
        try {
            trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());


            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
            keyStore.load(null);

            CertificateFactory cf = CertificateFactory.getInstance("X.509");
            Certificate cert = cf.generateCertificate(BarCodeSystemApplication.getContext().getAssets().open("certmgr.cer"));
            //设置自己的证书 多个证书时,alias不重复即可
            keyStore.setCertificateEntry("alias", cert);

            trustManagerFactory.init(keyStore);

            TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
            if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
                throw new IllegalStateException("Unexpected default trust managers:"
                        + Arrays.toString(trustManagers));
            }

            trustManager = (X509TrustManager) trustManagers[0];

            SSLContext sslContext = null;
            sslContext = SSLContext.getInstance("TLS");

            sslContext.init(null, new TrustManager[]{trustManager}, null);

            sslSocketFactory = sslContext.getSocketFactory();

        } catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException | CertificateException | IOException e) {
            e.printStackTrace();
        }

    }

    public static SSLSocketFactory getSllSocketFactory() {
        return instance.sslSocketFactory;
    }

    public static X509TrustManager getTrustManager() {
        return instance.trustManager;
    }

    public static HostnameVerifier getHostNameVerifier() {
        return instance.hostnameVerifier;
    }

    public static HttpsTransportSE getHttpsTransport() {
        HttpsURLConnection.setDefaultHostnameVerifier(instance.hostnameVerifier);
        HttpsURLConnection.setDefaultSSLSocketFactory(instance.sslSocketFactory);

        String baseUrl = RetrofitModule.generateBaseUrl();
        Uri uri = Uri.parse(baseUrl);
        int timeOut = SpHelper.getInstance(BarCodeSystemApplication.getContext()).getSp().getInt(AppConstants.SP_KEY_TIME_OUT, Command.CONN_TIME_OUT);
        return new HttpsTransportSE(uri.getHost(), uri.getPort(), uri.getPath(), timeOut);
    }

    public static HttpsTransportSE getHttpsTransport(String url) {
        HttpsURLConnection.setDefaultHostnameVerifier(instance.hostnameVerifier);
        HttpsURLConnection.setDefaultSSLSocketFactory(instance.sslSocketFactory);

        Uri uri = Uri.parse(url);
        int timeOut = SpHelper.getInstance(BarCodeSystemApplication.getContext()).getSp().getInt(AppConstants.SP_KEY_TIME_OUT, Command.CONN_TIME_OUT);
        return new HttpsTransportSE(uri.getHost(), uri.getPort(), uri.getPath(), timeOut);
    }
}

okhttp如何配置自签名证书

okhttp配置自签名证书,相当简单,从OKHTTP官网或者其他博客找代码一找一大堆。这里需要说明的是,在构建HTTPClient时,除了要sslSocketFactory设置SocketFactory和TrustManager之外,还要设置hostnameVerifier,一般在其实现的方法中直接返回true即可,如果有需要结合服务器返回证书,做其他校验的,参照OkHttp框架中OkHostnameVerifier.INSTANCE实现思路即可。
我这里的代码实现:

					//省略 。。。
					new OkHttpClient.Builder()
                    .addInterceptor(logging)//添加打印拦截器
                    .connectTimeout(timeOut, TimeUnit.MILLISECONDS)//设置请求超时时间
                    .readTimeout(0, TimeUnit.MILLISECONDS)
                    .writeTimeout(0, TimeUnit.MILLISECONDS)
                    .retryOnConnectionFailure(true)//设置出现错误进行重新连接。
                    .sslSocketFactory(SSLHelper.getSllSocketFactory(), SSLHelper.getTrustManager())
                    .hostnameVerifier(SSLHelper.getHostNameVerifier())
                    //省略 。。。

ksoap2-android如何配置自签名证书

ksoap做安卓端的网络请求框架,现在很少见了,所以靠谱的参考不是很好找。于是就翻阅了ksoap2-android官网: ksoap2-andorid官网
然而官网上的文档多少有点坑(项目里版本是3.0.0,也许是文档和项目版本不对应的关系)
在官网Tips and Tricks一节,找到了ksoap2-android有关https细节:

How to set the SSLSocketFactory on a https connection in order to allow self-signed certificates read from a KeyStore
public class ConnectionWithSelfSignedCertificate {

  private KeyStore keyStore;

  public ConnectionWithSelfSignedCertificate(KeyStore keyStore) {
    this.keyStore = keyStore;
  }

  public void dummy(String host, int port, String file, int timeout) throws Exception {
    SoapObject client = new SoapObject("", "dummy");
    SoapSerializationEnvelope envelope = new SoapSerializationEnvelope(SoapEnvelope.VER11);
    envelope.bodyOut = client;
    HttpsTransportSE transport = new HttpsTransportSE(host, port, file, timeout);
    ((HttpsServiceConnectionSE) transport.getConnection()).setSSLSocketFactory(getSSLSocketFactory());
    transport.call("", envelope);
  }

  private SSLSocketFactory getSSLSocketFactory() throws KeyStoreException, NoSuchAlgorithmException, KeyManagementException {
    TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
    tmf.init(keyStore);
    SSLContext context = SSLContext.getInstance("SSL");
    context.init(null, tmf.getTrustManagers(), null);
    return context.getSocketFactory();
  }
}

然而细节也不细,少了KeyStore初始化相关代码。悲催的我是先适配的ksoap,然后适配的okhttp。等到我找到正确的KeyStore加载证书文件相关代码之后,更坑的来了:官网示例代码中那个dummy方法,是模拟的一个https请求,我仿照最后三行代码的写法,整半天,https请求也没发出去,最后又折腾好一通,才找到了,原来setSSLSocketFactory这一步,不是在 ((HttpsServiceConnectionSE) transport.getConnection())这一步调用,而是要通过HttpsURLConnection的静态方法调用的,吐~~~~

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值