Android 上 Https 双向通信— 深入理解KeyManager 和 TrustManagers

在Android 上http 访问采用双向ssl 认证是一种很常见的场景。这种通常是Android作为客户端,访问后台服务器。Android 作为服务端的情况比较少见。 下面就谈谈Android 同时作为服务端和客户端的情况。

Android 客户端的配置

Android 作为客户端https 通信,通常需要一个SSLContext, SSLContext 需要配置一个 TrustManager,如果是双向通信,还需要一个 KeyManager。

  1. 单行https TrustManager
  2. 双向https TrustManager KeyManager
  3. KeyManager 负责提供证书和私钥,证书发给对方peer
  4. TrustManager 负责验证peer 发来的证书。

生成SSLContext

    KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
    TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());

    kmf.init(mKeyStore, mKeyPass.toCharArray());
    tmf.init(mTrustStore);

    SSLContext sslContext = SSLContext.getInstance("TLS");
    sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);

和Http 客户端关联

OkHttp 客户端如下:

        SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
        X509TrustManager x509TrustManager = Platform.get().trustManager(sslSocketFactory);

        OkHttpClient okHttpClient = new OkHttpClient
                .Builder()
                .addInterceptor(httpLoggingInterceptor)
                .sslSocketFactory(sslSocketFactory, x509TrustManager)
                .build();

        mRetrofit = new Retrofit.Builder()
                .baseUrl(mBaseHost)
                .client(okHttpClient)
                .addConverterFactory(GsonConverterFactory.create())
                .build();

AndroidAsync 的客户端:

AsyncHttpClient.getDefaultInstance().getSSLSocketMiddleware().setSSLContext(sslContext);
AsyncHttpClient.getDefaultInstance().getSSLSocketMiddleware().setTrustManagers(tmf.getTrustManagers());

如果是客户单的话配置还是比较的简单明了的

Android 作为Https 的服务端

在技术选型的时候选择了 AndroidAsync 作为服务端的框架。另外一个是 NanoHTTPD

AndroidAsync 和 NanoHTTPD 的对比

因为要同时实现客户端和服务端,而且AndroidAsync 支持异步,更符合现在的Android 趋势。

AndroidService 支持客户端证书请求

客户端按照上面的配置了一下,服务端也如法炮制sslContext,AndroidAsync 提供了一个SSLTests 的测试用例,采用自签名证书方式。

        AsyncHttpServer httpServer = new AsyncHttpServer();
        httpServer.listenSecure(8888, sslContext);
        httpServer.get("/", new HttpServerRequestCallback() {
            @Override
            public void onRequest(AsyncHttpServerRequest request, AsyncHttpServerResponse response) {
                response.send("hello");
            }
        });

只能实现单向Https ,无法双向。通过抓包对比,发现双向https 需要服务端向客户端发送一个 Certificate Request。但是服务端没有发送。Android 上ssl 握手是通过openssl 实现的。通过查阅一些论文,查看boringssl 源码,是一个变量没有设置导致 handshake 的时候服务端没有发送 Certificate Request。 修改boringssl 不太现实。换个思路这个变量是不是可以通过Java层控制。

public void listenSecure(final int port, final SSLContext sslContext) {
        AsyncServer.getDefault().listen(null, port, new ListenCallback() {
            @Override
            public void onAccepted(AsyncSocket socket) {
                AsyncSSLSocketWrapper.handshake(socket, null, port, sslContext.createSSLEngine(), null, null, false,
                new AsyncSSLSocketWrapper.HandshakeCallback() {
                    @Override
                    public void onHandshakeCompleted(Exception e, AsyncSSLSocket socket) {
                        if (socket != null)
                            mListenCallback.onAccepted(socket);
                    }
                });
            }

            ......
            
        });
    }

通过对 AndroidAsync AsyncHttpServer 的实现分析,SSLContext的方法没有我们要控制的功能。但是ssl 握手的时候创建了一个SSLEngine。SSLEngine 的方法比较多的。

SSLEngine.setNeedClientAuth(true);

这个方法看起来比较靠谱。但是AndroidAsync 框架并没有提供API,想办法把这个类拿出来重写。服务端用SslAsyncHttpServer 替换AsyncHttpServer, Certificate Request 终于发出来了。

class SslAsyncHttpServer extends AsyncHttpServer {
    private static final String TAG = "SslAsyncHttpServer";

    private SSLEngine mSSLEngine

    @Override
    public void listenSecure(final int port, final SSLContext sslContext) {
        AsyncServer.getDefault().listen(null, port, new ListenCallback() {
            @Override
            public void onAccepted(AsyncSocket socket) {
                mSSLEngine = sslContext.createSSLEngine();
                mSSLEngine.setNeedClientAuth(true);
                AsyncSSLSocketWrapper.handshake(socket, null, port, mSSLEngine, null, null, false,
                        new AsyncSSLSocketWrapper.HandshakeCallback() {
                            @Override
                            public void onHandshakeCompleted(Exception e, AsyncSSLSocket socket) {
                                if (socket != null)
                                    getListenCallback().onAccepted(socket);
                            }
                        });
            }

            @Override
            public void onListening(AsyncServerSocket socket) {
                getListenCallback().onListening(socket);
            }

            @Override
            public void onCompleted(Exception ex) {
                getListenCallback().onCompleted(ex);
            }
        });
    }
}

使用认证链做认证

在生产环境中 对证书的校验更为严格,通常采用证书链的方式。还是上面的code, 采用证书链的方式以后. handshake 失败。

04-27 08:43:33.093 6881-6903/com.louie.certtest W/System.err: javax.net.ssl.SSLHandshakeException: Handshake failed
04-27 08:43:33.093 6881-6903/com.louie.certtest W/System.err:     at com.android.org.conscrypt.OpenSSLEngineImpl.unwrap(OpenSSLEngineImpl.java:441)
04-27 08:43:33.093 6881-6903/com.louie.certtest W/System.err:     at javax.net.ssl.SSLEngine.unwrap(SSLEngine.java:1270)
04-27 08:43:33.093 6881-6903/com.louie.certtest W/System.err:     at com.koushikdutta.async.AsyncSSLSocketWrapper$5.onDataAvailable(AsyncSSLSocketWrapper.java:194)
04-27 08:43:33.093 6881-6903/com.louie.certtest W/System.err:     at com.koushikdutta.async.Util.emitAllData(Util.java:23)
04-27 08:43:33.093 6881-6903/com.louie.certtest W/System.err:     at com.koushikdutta.async.AsyncNetworkSocket.onReadable(AsyncNetworkSocket.java:152)
04-27 08:43:33.093 6881-6903/com.louie.certtest W/System.err:     at com.koushikdutta.async.AsyncServer.runLoop(AsyncServer.java:821)
04-27 08:43:33.093 6881-6903/com.louie.certtest W/System.err:     at com.koushikdutta.async.AsyncServer.run(AsyncServer.java:658)
04-27 08:43:33.093 6881-6903/com.louie.certtest W/System.err:     at com.koushikdutta.async.AsyncServer.access$800(AsyncServer.java:44)
04-27 08:43:33.093 6881-6903/com.louie.certtest W/System.err:     at com.koushikdutta.async.AsyncServer$14.run(AsyncServer.java:600)
04-27 08:43:33.094 6881-6903/com.louie.certtest W/System.err: Caused by: javax.net.ssl.SSLProtocolException: SSL handshake terminated: ssl=0xb31d4fc0: Failure in SSL library, usually a protocol error
04-27 08:43:33.094 6881-6903/com.louie.certtest W/System.err: error:100000c0:SSL routines:OPENSSL_internal:PEER_DID_NOT_RETURN_A_CERTIFICATE (external/boringssl/src/ssl/s3_srvr.c:1945 0xa3b68196:0x00000000)
04-27 08:43:33.094 6881-6903/com.louie.certtest W/System.err:     at com.android.org.conscrypt.NativeCrypto.SSL_do_handshake_bio(Native Method)
04-27 08:43:33.094 6881-6903/com.louie.certtest W/System.err:     at com.android.org.conscrypt.OpenSSLEngineImpl.unwrap(OpenSSLEngineImpl.java:426)
04-27 08:43:33.094 6881-6903/com.louie.certtest W/System.err:   ... 8 more

wireshark 抓包后发现,服务端发送了 TCP 的FIN,查看整个握手过程,服务端发送 Certificate Request 后,客户端也发送了 "Certificate",但是服务端随后就发送了 Fin。又是一个让人头疼的问题。从源码来看,确实是服务端 调用了close.

4 0.007445 127.0.0.1 127.0.0.1 TLSv1.2 205 Client Hello

6 0.022138 127.0.0.1 127.0.0.1 TLSv1.2 1652 Server Hello, Certificate, Server Key Exchange, Certificate Request, Server Hello Done

8 0.029033 127.0.0.1 127.0.0.1 TLSv1.2 204 Certificate, Client Key Exchange, Change Cipher Spec, Hello Request, Hello Request

9 0.031817 127.0.0.1 127.0.0.1 TCP 66 6666→51878 [FIN, ACK] Seq=1587 Ack=278 Win=131008 Len=0 TSval=1856446 TSecr=1856446

查到了 ssl 握手的 RFC 文档

The TLS ProtocolVersion 1.0

7.4.6. Client certificate

   When this message will be sent:
       This is the first message the client can send after receiving a
       server hello done message. This message is only sent if the
       server requests a certificate. If no suitable certificate is
       available, the client should send a certificate message
       containing no certificates. If client authentication is required
       by the server for the handshake to continue, it may respond with
       a fatal handshake failure alert. Client certificates are sent
       using the Certificate structure defined in Section 7.4.2.

然后再看 在wireshark 中看客户端回的 Certificate 字段,长度居然为0.

想看下完整的ssl 握手过程,但是Android上并没有SSL 握手的详细日志。

使用hugo

stackoverflow 上有一篇帖子清奇:

Client Certificate not working from Android - How to debug?

关于hugo 的详细使用参考
hugo

服务端发送证书

04-27 04:05:02.025 3682-3699/com.louie.certtest V/SslX509KeyManager: ⇢ chooseServerAlias(s="EC", principals=null, socket=null) [Thread:"AsyncServer"]
04-27 04:05:02.025 3682-3699/com.louie.certtest V/SslX509KeyManager: ⇠ chooseServerAlias [0ms] = null
04-27 04:05:02.026 3682-3699/com.louie.certtest V/SslX509KeyManager: ⇢ chooseServerAlias(s="RSA", principals=null, socket=null) [Thread:"AsyncServer"]
04-27 04:05:02.026 3682-3699/com.louie.certtest V/SslX509KeyManager: ⇠ chooseServerAlias [0ms] = "1"
04-27 04:05:02.026 3682-3699/com.louie.certtest V/SslX509KeyManager: ⇢ getPrivateKey(s="1") [Thread:"AsyncServer"]
04-27 04:05:02.026 3682-3699/com.louie.certtest V/SslX509KeyManager: ⇠ getPrivateKey [0ms] = RSA Private CRT Key
  1. 从日志上看, 第 1 2 3 4 行是服务端需要发送 Certificate, 在KeyStore 中选择和是的证书Alias
  2. 5 6 行根据选择的Alias 获取PrivateKey.
04-27 04:05:02.026 3682-3699/com.louie.certtest V/SslX509KeyManager: ⇢ getCertificateChain(s="1") [Thread:"AsyncServer"]
04-27 04:05:02.030 3682-3699/com.louie.certtest V/SslX509KeyManager: ⇠ getCertificateChain [0ms] = [Certificate:
                                                                         Data:
                                                                             Version: 3 (0x2)
                                                                             Serial Number:
                                                                                 21:dd:e7:2c:8c:95:d9:f1
                                                                         Signature Algorithm: sha256WithRSAEncryption
                                                                             Issuer: CN=XXX_Web_test, O=XXX, C=US
                                                                             Validity
                                                                                 Not Before: Mar 26 14:38:49 2018 GMT
                                                                                 Not After : Jan  4 20:48:34 2037 GMT
                                                                             Subject: CN=ae86.XXXXXXX-local.com
  1. 接下来的日志表示找到服务端的证书,服务端的证书会发送给客户端。

服务端发送 Certificate Request

服务端首先根据KeyStore 中的证书链 找出客户端需要发送证书的issure. 从日志上看是一个 Intermediate CA:

04-27 04:05:02.030 3682-3699/com.louie.certtest V/SslX509TrustManager: ⇢ getAcceptedIssuers() [Thread:"AsyncServer"]
04-27 04:05:02.043 3682-3699/com.louie.certtest V/SslX509TrustManager: ⇠ getAcceptedIssuers [0ms] = [Certificate:
                                                                           Data:
                                                                               Version: 3 (0x2)
                                                                               Serial Number:
                                                                                   74:0e:7c:31:e5:5e:2c:9d
                                                                           Signature Algorithm: sha256WithRSAEncryption
                                                                               Issuer: CN=XXX Root CA, O=XXX, C=US
                                                                               Validity
                                                                                   Not Before: Jan  4 20:48:34 2017 GMT
                                                                                   Not After : Jan  4 20:48:34 2037 GMT
                                                                               Subject: CN=XXX Intermediate CA, O=XXX, C=US
                                                                              

客户端校验

客户端校验服务端证书:

04-27 08:35:41.909 6640-6660/com.louie.certtest V/SslX509TrustManager: ⇢ checkServerTrusted(chain=[Certificate:

客户端发送证书

客户端根据服务端发送的 Certificate Request 选择合适的证书
从日志上可以看出:

服务端发送的证书 Subject 为:

  1. C=US, O=XXX, CN=XXX Intermediate CA,
  2. C=US, O=XXX, CN=XXX Root CA,
  3. C=US, O=XXX, CN=XXX Root CA]

客户端证书的Issure为:

  1. C=US, O=XXX, CN=XXX_Vehicle_test

客户端没有找到合适的证书,所以发送的证书长度为0。

04-27 08:35:41.914 6640-6660/com.louie.certtest V/SslX509KeyManager: ⇢ chooseClientAlias(strings=["EC", "RSA"], principals=[C=US, O=XXX, CN=XXX Intermediate CA, C=US, O=XXX, CN=XXX Root CA, C=US, O=XXX, CN=XXX Root CA], socket=null) [Thread:"AsyncServer"]
04-27 08:35:41.914 6640-6660/com.louie.certtest V/KeyManagerImpl: ⇢ printVar(name="issuersList", object=[C=US, O=XXX, CN=XXX Intermediate CA, C=US, O=XXX, CN=XXX Root CA, C=US, O=XXX, CN=XXX Root CA]) [Thread:"AsyncServer"]
04-27 08:35:41.915 6640-6660/com.louie.certtest V/KeyManagerImpl: ⇠ printVar [0ms]
04-27 08:35:41.915 6640-6660/com.louie.certtest V/KeyManagerImpl: ⇢ printVar(name="issuerFromChain", object=C=US, O=XXX, CN=XXX_Vehicle_test) [Thread:"AsyncServer"]
04-27 08:35:41.915 6640-6660/com.louie.certtest V/KeyManagerImpl: ⇠ printVar [0ms]
04-27 08:35:41.915 6640-6660/com.louie.certtest V/KeyManagerImpl: ⇢ printVar(name="alias", object="1") [Thread:"AsyncServer"]
04-27 08:35:41.915 6640-6660/com.louie.certtest V/KeyManagerImpl: ⇠ printVar [0ms]
04-27 08:35:41.915 6640-6660/com.louie.certtest V/SslX509KeyManager: ⇠ chooseClientAlias [0ms] = null

concrypt 补丁

找到原因后,看下服务端为发送的 Certificate Request 为什么不正确。
通过debug , 服务端调用 在 SSLParametersImpl.java 中的 setCertificateValidation 调用 trustManager.getAcceptedIssuers()。
然后调用 encodeIssuerX509Principals 函数。

void setCertificateValidation(long sslNativePointer) throws IOException {
        if(!this.client_mode) {
            boolean certRequested;
            
            。。。。。。

            if(certRequested) {
                X509TrustManager trustManager = this.getX509TrustManager();
                X509Certificate[] issuers = trustManager.getAcceptedIssuers();
                if(issuers != null && issuers.length != 0) {
                    byte[][] issuersBytes;
                    try {
                        issuersBytes = encodeIssuerX509Principals(issuers);
                    } catch (CertificateEncodingException var8) {
                        throw new IOException("Problem encoding principals", var8);
                    }

                    NativeCrypto.SSL_set_client_CA_list(sslNativePointer, issuersBytes);
                }
            }
        }

    }

在encodeIssuerX509Principals 中调用getIssuerX500Principal 获取证书的Issuere.如果我们有三个证书组成认证链:

  1. [subject=RootCA, issure=RootCA],
  2. [subject=SecondCA, issure=RootCA]
  3. [subject=ThirdCA, issure=SecondCA]

getIssuerX500Principal 获取到的是

[RootCA, RootCA, SecondCA]

正确的做法为:getSubjectX500Principal
这样获取到的为:

[RootCA, SecondCA, ThirdCA]

    static byte[][] encodeIssuerX509Principals(X509Certificate[] certificates) throws CertificateEncodingException {
        byte[][] principalBytes = new byte[certificates.length][];

        for(int i = 0; i < certificates.length; ++i) {
            principalBytes[i] = certificates[i].getIssuerX500Principal().getEncoded();
        }

        return principalBytes;
    }

Two way ssl uses trustchain, android as a service 提交

在Android 8.0 上测试,发现还是有这个问题。Androd作为客户端的场景比较常见,
作为服务端比较少见。向google 提交了一个补丁:

Two way ssl uses trustchain, android as a service

https 握手过程中的KeyManager 和TrustManager 调用

https SSL 握手过程.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容