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 負責提供證書和私鑰顶掉,證書發(fā)給對方peer
  4. TrustManager 負責驗證peer 發(fā)來的證書。

生成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 客戶端關(guān)聯(lián)

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 的服務端

在技術(shù)選型的時候選擇了 AndroidAsync 作為服務端的框架挑胸。另外一個是 NanoHTTPD

AndroidAsync 和 NanoHTTPD 的對比

因為要同時實現(xiàn)客戶端和服務端痒筒,而且AndroidAsync 支持異步,更符合現(xiàn)在的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");
            }
        });

只能實現(xiàn)單向Https ,無法雙向。通過抓包對比螟左,發(fā)現(xiàn)雙向https 需要服務端向客戶端發(fā)送一個 Certificate Request啡浊。但是服務端沒有發(fā)送觅够。Android 上ssl 握手是通過openssl 實現(xiàn)的。通過查閱一些論文巷嚣,查看boringssl 源碼喘先,是一個變量沒有設置導致 handshake 的時候服務端沒有發(fā)送 Certificate Request。 修改boringssl 不太現(xiàn)實廷粒。換個思路這個變量是不是可以通過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 的實現(xiàn)分析,SSLContext的方法沒有我們要控制的功能坝茎。但是ssl 握手的時候創(chuàng)建了一個SSLEngine树枫。SSLEngine 的方法比較多的。

SSLEngine.setNeedClientAuth(true);

這個方法看起來比較靠譜景东。但是AndroidAsync 框架并沒有提供API砂轻,想辦法把這個類拿出來重寫。服務端用SslAsyncHttpServer 替換AsyncHttpServer斤吐, Certificate Request 終于發(fā)出來了搔涝。

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);
            }
        });
    }
}

使用認證鏈做認證

在生產(chǎn)環(huán)境中 對證書的校驗更為嚴格,通常采用證書鏈的方式和措。還是上面的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 抓包后發(fā)現(xiàn),服務端發(fā)送了 TCP 的FIN派阱,查看整個握手過程诬留,服務端發(fā)送 Certificate Request 后,客戶端也發(fā)送了 "Certificate"贫母,但是服務端隨后就發(fā)送了 Fin文兑。又是一個讓人頭疼的問題。從源碼來看腺劣,確實是服務端 調(diào)用了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?

關(guān)于hugo 的詳細使用參考
hugo

服務端發(fā)送證書

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 行是服務端需要發(fā)送 Certificate, 在KeyStore 中選擇和是的證書Alias
  2. 5 6 行根據(jù)選擇的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. 接下來的日志表示找到服務端的證書,服務端的證書會發(fā)送給客戶端趾断。

服務端發(fā)送 Certificate Request

服務端首先根據(jù)KeyStore 中的證書鏈 找出客戶端需要發(fā)送證書的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:

客戶端發(fā)送證書

客戶端根據(jù)服務端發(fā)送的 Certificate Request 選擇合適的證書
從日志上可以看出:

服務端發(fā)送的證書 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

客戶端沒有找到合適的證書拒名,所以發(fā)送的證書長度為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 補丁

找到原因后芋酌,看下服務端為發(fā)送的 Certificate Request 為什么不正確增显。
通過debug , 服務端調(diào)用 在 SSLParametersImpl.java 中的 setCertificateValidation 調(diào)用 trustManager.getAcceptedIssuers()隔嫡。
然后調(diào)用 encodeIssuerX509Principals 函數(shù)甸怕。

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 中調(diào)用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 上測試,發(fā)現(xiàn)還是有這個問題荡含。Androd作為客戶端的場景比較常見咒唆,
作為服務端比較少見。向google 提交了一個補妒鸵骸:

Two way ssl uses trustchain, android as a service

https 握手過程中的KeyManager 和TrustManager 調(diào)用

https SSL 握手過程.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末全释,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子误债,更是在濱河造成了極大的恐慌浸船,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,198評論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件寝蹈,死亡現(xiàn)場離奇詭異李命,居然都是意外死亡,警方通過查閱死者的電腦和手機箫老,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評論 3 398
  • 文/潘曉璐 我一進店門封字,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人耍鬓,你說我怎么就攤上這事阔籽。” “怎么了牲蜀?”我有些...
    開封第一講書人閱讀 167,643評論 0 360
  • 文/不壞的土叔 我叫張陵仿耽,是天一觀的道長。 經(jīng)常有香客問我各薇,道長项贺,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,495評論 1 296
  • 正文 為了忘掉前任峭判,我火速辦了婚禮开缎,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘林螃。我一直安慰自己奕删,他們只是感情好,可當我...
    茶點故事閱讀 68,502評論 6 397
  • 文/花漫 我一把揭開白布疗认。 她就那樣靜靜地躺著完残,像睡著了一般伏钠。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上谨设,一...
    開封第一講書人閱讀 52,156評論 1 308
  • 那天熟掂,我揣著相機與錄音,去河邊找鬼扎拣。 笑死赴肚,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的二蓝。 我是一名探鬼主播誉券,決...
    沈念sama閱讀 40,743評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼刊愚!你這毒婦竟也來了踊跟?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,659評論 0 276
  • 序言:老撾萬榮一對情侶失蹤鸥诽,失蹤者是張志新(化名)和其女友劉穎琴锭,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體衙传,經(jīng)...
    沈念sama閱讀 46,200評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡决帖,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,282評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了蓖捶。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片地回。...
    茶點故事閱讀 40,424評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖俊鱼,靈堂內(nèi)的尸體忽然破棺而出刻像,到底是詐尸還是另有隱情,我是刑警寧澤并闲,帶...
    沈念sama閱讀 36,107評論 5 349
  • 正文 年R本政府宣布细睡,位于F島的核電站,受9級特大地震影響帝火,放射性物質(zhì)發(fā)生泄漏溜徙。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,789評論 3 333
  • 文/蒙蒙 一犀填、第九天 我趴在偏房一處隱蔽的房頂上張望蠢壹。 院中可真熱鬧,春花似錦九巡、人聲如沸图贸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽疏日。三九已至偿洁,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間沟优,已是汗流浹背涕滋。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留净神,地道東北人。 一個月前我還...
    沈念sama閱讀 48,798評論 3 376
  • 正文 我出身青樓溉委,卻偏偏與公主長得像鹃唯,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子瓣喊,可洞房花燭夜當晚...
    茶點故事閱讀 45,435評論 2 359

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,262評論 25 707
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理坡慌,服務發(fā)現(xiàn),斷路器藻三,智...
    卡卡羅2017閱讀 134,693評論 18 139
  • 目錄 準備 分析2.1. 三次握手2.2. 創(chuàng)建 HTTP 代理(非必要)2.3. TLS/SSL 握手2.4. ...
    RunAlgorithm閱讀 38,288評論 12 117
  • “我額娘在怎么了?額娘也大不過皇后去啊玷或?再說了旺矾,我額娘那性子,年節(jié)都不愿意去宮里應卯的逗概,怕是原來咱們這位娘娘也沒少...
    籽鹽閱讀 175評論 0 1
  • 趙慧姿閱讀 124評論 3 1