文章目录

  • Android 客户端的配置
    • 生成SSLContext
    • 和Http 客户端关联
  • Android 作为Https 的服务端
    • AndroidService 支持客户端证书请求
  • 使用认证链做认证
  • 使用hugo 观察SSL handshark 过程
    • 服务端发送证书
    • 服务端发送 **Certificate Request**
    • 客户端校验
    • 客户端发送证书
    • concrypt 库的bug
    • Android Patch
  • https 握手过程中的KeyManager 和TrustManager 调用

在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() {@Overridepublic 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() {@Overridepublic void onAccepted(AsyncSocket socket) {AsyncSSLSocketWrapper.handshake(socket, null, port, sslContext.createSSLEngine(), null, null, false,new AsyncSSLSocketWrapper.HandshakeCallback() {@Overridepublic 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@Overridepublic void listenSecure(final int port, final SSLContext sslContext) {AsyncServer.getDefault().listen(null, port, new ListenCallback() {@Overridepublic void onAccepted(AsyncSocket socket) {mSSLEngine = sslContext.createSSLEngine();mSSLEngine.setNeedClientAuth(true);AsyncSSLSocketWrapper.handshake(socket, null, port, mSSLEngine, null, null, false,new AsyncSSLSocketWrapper.HandshakeCallback() {@Overridepublic void onHandshakeCompleted(Exception e, AsyncSSLSocket socket) {if (socket != null)getListenCallback().onAccepted(socket);}});}@Overridepublic void onListening(AsyncServerSocket socket) {getListenCallback().onListening(socket);}@Overridepublic 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 certificateWhen this message will be sent:This is the first message the client can send after receiving aserver hello done message. This message is only sent if theserver requests a certificate. If no suitable certificate isavailable, the client should send a certificate messagecontaining no certificates. If client authentication is requiredby the server for the handshake to continue, it may respond witha fatal handshake failure alert. Client certificates are sentusing the Certificate structure defined in Section 7.4.2.

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

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

使用hugo 观察SSL handshark 过程

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:f1Signature Algorithm: sha256WithRSAEncryptionIssuer: CN=XXX_Web_test, O=XXX, C=USValidityNot Before: Mar 26 14:38:49 2018 GMTNot After : Jan  4 20:48:34 2037 GMTSubject: 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:9dSignature Algorithm: sha256WithRSAEncryptionIssuer: CN=XXX Root CA, O=XXX, C=USValidityNot Before: Jan  4 20:48:34 2017 GMTNot After : Jan  4 20:48:34 2037 GMTSubject: 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 库的bug

找到原因后,看下服务端为发送的 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;}

Android Patch

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

Two way ssl uses trustchain, android as a service

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

Android 上 Https 双向通信— 深入理解KeyManager 和 TrustManagers相关推荐

  1. 下载 https://github.com/android 上的全部源代码

    从 https://android.googlesource.com/ 上弄源代码下来真是不容易,不但得用 vpn,repo 还得半天.github 的速度就快多了,关键是不用 vpn,随时可以下载. ...

  2. Android 上的低功耗蓝牙实践

    转载自:https://www.race604.com/android-ble-in-action/ 我今天分享的主题是 Android 上低功耗蓝牙的实践.这个主题比较小众.我在过去的一年多的时间里 ...

  3. 【转】iOS类似Android上toast效果

    原文网址:http://m.blog.csdn.net/article/details?id=50478737 做过Android开发的人都知道toast,它会在界面上显示一排黑色背景的文字,用于提示 ...

  4. Tensorflow object detection API 搭建自己的目标检测模型并迁移到Android上

    参考链接:https://blog.csdn.net/dy_guox/article/details/79111949 之前参考上述一系列博客在Windows10下面成功运行了TensorFlow A ...

  5. qemuuser模式运行linux,在Android上使用qemu-user运行可执行文件

    作者:寻禹@阿里聚安全 前言 QEMU简要介绍: QEMU可以解释执行可执行程序.既然QEMU可以解释执行可执行程序,那么QEMU就能够知道执行了哪些指令,从而可以跟踪指令的执行.QEMU编译出来的结 ...

  6. [forwarding]Android上dip、dp、px、sp等单位说明

    dip  device independent pixels(设备独立像素). 不同设备不同的显示效果,这个和设备硬件有关,一般我们为了支持WVGA.HVGA和QVGA 推荐使用这个,不依赖像素. 在 ...

  7. android loadsvm raw,OpenCV机器学习:Android上利用SVM实现手写体数字识别

    这篇博客是之前那篇在win7上用OpenCV的SVM分类器做MNIST手写数字识别的后续.用MNIST数据集做SVM训练和测试的细节可以移步那篇博客进行了解. 0.开发环境 这篇文章的思路是将Wind ...

  8. 如何在Android上显示wmf的文件

    菜鸟进场,方圆十里,寸草不生 最近定制化一个app,需要显示wmf的文件,但是Android上无法对这种文件的直接显示,所以就需要对文件进行一个转换,网上对这方面的需求很少,所以简单的记录一下,就讲讲 ...

  9. OpenGL.ES在Android上的简单实践:11-全景(索引-深度测试)

    OpenGL.ES在Android上的简单实践:11-全景(正方体-索引-深度测试) 0.全景图要怎么看? What is 全景?可能很多人单看这名字不太清楚.但看到下面的图的时候就噢的一声~瞬间廓然 ...

  10. Android上百实例源码分析以及开源分析集合打包

    感谢网友banketree的收集,压缩包的内容如下: 1.360新版特性界面源代码 实现了360新版特性界面的效果,主要涉及到Qt的一些事件处理与自定义控件.但源码好像是c++. 2.aidl跨进程调 ...

最新文章

  1. HTML的数据 转成 JSON数据中的 因HTML有大量及其它特殊符号会把JSON字符串截断该怎么...
  2. CodeForces 164C Machine Programming 费用流
  3. LINUX:安装下载源码用的hg
  4. 马尾物联网企业主导或参与制定修订各级标准达60项
  5. python--查询PG数据库
  6. 优秀关卡设计的十个原则
  7. c4d打开没反应_(图文+视频)C4D野教程:总结四种制作漩涡样条的方法
  8. 怎么在gif表情包上添加文字?
  9. 自己制做python3.6精简绿色版
  10. 【Windows】之电脑之间共享文件(亲测可行)
  11. 基于PC的机器视觉系统设计
  12. mysql1291错误_当MySQL创建表格时出现 1291 - Column 'e_sxe' has duplicated value '?' in ENUM 错误...
  13. 表格进阶03—出纳日报表(表格,再次练习)
  14. Android直播软件开发中接入腾讯IM大概流程是怎样的
  15. RF自动化测试系列-第三篇 测试数据
  16. 举个例子丨什么是量子计算机?比常规计算机强在哪里?
  17. Python编程PTA题解——两数之和
  18. 凡是过去,皆为序章——NFT2.0的演进路径:金融化与商品化
  19. 如何减少手机辐射?——七招减少手机对您的危害
  20. CISAW证书发证机构是哪里?权威性如何?

热门文章

  1. android intent传文件,android如何用intent跳转到文件管理器
  2. 魅族4usb计算机连接,魅族MX4如何连接电脑 魅族MX4连接电脑方法
  3. grpc-go源码剖析七十四之什么情况下服务器端通知客户端,双方关闭链接?(goAway帧处理逻辑)
  4. 关联分析python代码
  5. JetBrains 全家桶的全流程详解
  6. torch.cuda
  7. css pseudo elements,CSS 伪元素 (Pseudo-elements)
  8. 关于 ‘cosylocal‘ 进程占满内存的问题
  9. nginx的反向代理和负载均衡
  10. 苹果Mac怎样清除dns缓存?