在Android 上http 訪問采用雙向ssl 認證是一種很常見的場景腮猖。這種通常是Android作為客戶端税稼,訪問后臺服務器。Android 作為服務端的情況比較少見奈附。 下面就談談Android 同時作為服務端和客戶端的情況全度。
Android 客戶端的配置
Android 作為客戶端https 通信,通常需要一個SSLContext斥滤, SSLContext 需要配置一個 TrustManager将鸵,如果是雙向通信,還需要一個 KeyManager佑颇。
- 單行https TrustManager
- 雙向https TrustManager KeyManager
- KeyManager 負責提供證書和私鑰顶掉,證書發(fā)給對方peer
- 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
因為要同時實現(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 文檔
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 2 3 4 行是服務端需要發(fā)送 Certificate, 在KeyStore 中選擇和是的證書Alias
- 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
- 接下來的日志表示找到服務端的證書,服務端的證書會發(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 為:
- C=US, O=XXX, CN=XXX Intermediate CA,
- C=US, O=XXX, CN=XXX Root CA,
- C=US, O=XXX, CN=XXX Root CA]
客戶端證書的Issure為:
- 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.如果我們有三個證書組成認證鏈:
- [subject=RootCA, issure=RootCA],
- [subject=SecondCA, issure=RootCA]
- [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