Android 上 Https 双向通信— 深入理解KeyManager 和 TrustManagers
在Android 上http 访问采用双向ssl 认证是一种很常见的场景。这种通常是Android作为客户端,访问后台服务器。Android 作为服务端的情况比较少见。 下面就谈谈Android 同时作为服务端和客户端的情况。
Android 客户端的配置
Android 作为客户端https 通信,通常需要一个SSLContext, SSLContext 需要配置一个 TrustManager,如果是双向通信,还需要一个 KeyManager。
- 单行https TrustManager
- 双向https TrustManager KeyManager
- KeyManager 负责提供证书和私钥,证书发给对方peer
- 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 failed04-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 error04-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] = null04-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 行是服务端需要发送 Certificate, 在KeyStore 中选择和是的证书Alias
- 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
- 接下来的日志表示找到服务端的证书,服务端的证书会发送给客户端。
服务端发送 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 为:
- 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
客户端没有找到合适的证书,所以发送的证书长度为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.如果我们有三个证书组成认证链:
- [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 上测试,发现还是有这个问题。Androd作为客户端的场景比较常见,
作为服务端比较少见。向google 提交了一个补丁:
Two way ssl uses trustchain, android as a service
https 握手过程中的KeyManager 和TrustManager 调用
https SSL 握手过程.png更多相关文章
- 【Android】socket通信【客户端访问】
- 来往 android客户端发布
- Android SDK 证书没接受问题
- Android之开源中国客户端源码分析(二)
- OSCHINA Android 客户端 - 手机相关软件 - 开源中国
- Android证书创建之 keytool 错误:java.io.IOException:Incorrect
- Android腾讯微薄客户端开发教程汇总
- struts2服务端与android交互
- Android笔记:Socket客户端收发数据