【问题标题】:Websocket SSL handshake failureWebsocket SSL 握手失败
【发布时间】:2015-03-16 16:31:11
【问题描述】:

我有用于安全 websocket 连接的 spring-boot Tomcat 服务器。服务器接受 Android 4.4、iOS、Firefox 和 Chrome 客户端而不会失败,并带有授权签名的证书。但是,Android 5.0 无法通过 SSL 握手。

Caused by: javax.net.ssl.SSLHandshakeException: Handshake failed
        at com.android.org.conscrypt.OpenSSLEngineImpl.unwrap(OpenSSLEngineImpl.java:436)
        at javax.net.ssl.SSLEngine.unwrap(SSLEngine.java:1006)
        at org.glassfish.grizzly.ssl.SSLConnectionContext.unwrap(SSLConnectionContext.java:172)
        at org.glassfish.grizzly.ssl.SSLUtils.handshakeUnwrap(SSLUtils.java:263)
        at org.glassfish.grizzly.ssl.SSLBaseFilter.doHandshakeStep(SSLBaseFilter.java:603)
        at org.glassfish.grizzly.ssl.SSLFilter.doHandshakeStep(SSLFilter.java:312)
        at org.glassfish.grizzly.ssl.SSLBaseFilter.doHandshakeStep(SSLBaseFilter.java:552)
        at org.glassfish.grizzly.ssl.SSLBaseFilter.handleRead(SSLBaseFilter.java:273)
        at org.glassfish.grizzly.filterchain.ExecutorResolver$9.execute(ExecutorResolver.java:119)
        at org.glassfish.grizzly.filterchain.DefaultFilterChain.executeFilter(DefaultFilterChain.java:284)
        at org.glassfish.grizzly.filterchain.DefaultFilterChain.executeChainPart(DefaultFilterChain.java:201)
        at org.glassfish.grizzly.filterchain.DefaultFilterChain.execute(DefaultFilterChain.java:133)
        at org.glassfish.grizzly.filterchain.DefaultFilterChain.process(DefaultFilterChain.java:112)
        at org.glassfish.grizzly.ProcessorExecutor.execute(ProcessorExecutor.java:77)
        at org.glassfish.grizzly.nio.transport.TCPNIOTransport.fireIOEvent(TCPNIOTransport.java:561)
        at org.glassfish.grizzly.strategies.AbstractIOStrategy.fireIOEvent(AbstractIOStrategy.java:112)
        at org.glassfish.grizzly.strategies.WorkerThreadIOStrategy.run0(WorkerThreadIOStrategy.java:117)
        at org.glassfish.grizzly.strategies.WorkerThreadIOStrategy.access$100(WorkerThreadIOStrategy.java:56)
        at org.glassfish.grizzly.strategies.WorkerThreadIOStrategy$WorkerThreadRunnable.run(WorkerThreadIOStrategy.java:137)
        at org.glassfish.grizzly.threadpool.AbstractThreadPool$Worker.doWork(AbstractThreadPool.java:565)
        at org.glassfish.grizzly.threadpool.AbstractThreadPool$Worker.run(AbstractThreadPool.java:545)
at java.lang.Thread.run(Thread.java:818)
 Caused by: javax.net.ssl.SSLProtocolException: SSL handshake terminated: ssl=0xa1f34200: Failure in SSL library, usually a protocol error
error:1408E0F4:SSL routines:SSL3_GET_MESSAGE:unexpected message (external/openssl/ssl/s3_both.c:498 0xac526e61:0x00000000)
        at com.android.org.conscrypt.NativeCrypto.SSL_do_handshake_bio(Native Method)
        at com.android.org.conscrypt.OpenSSLEngineImpl.unwrap(OpenSSLEngineImpl.java:423)

我认为问题在于changes in Android 5.0 Lollipop 导致的 TLS 或密码套件,而不是证书,因为其他客户端连接,但我无法弄清楚如何判断连接的客户端发生了什么,因为SSL debugging does not appear to be supported on Android. 该问题可能与this one 非常相似,这也尚未解决,但表明问题出在密码套件上。 Android 错误 88313 81603 developer-preview-1989 似乎表明 Android 实现是正确的,但服务器配置或密码套件的实现可能不正确。

我设置了以下服务器密码套件

server.ssl.ciphers = TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,TLS_RSA_WITH_AES_128_CBC_SHA,TLS_DHE_RSA_WITH_AES_128_CBC_SHA,TLS_DHE_DSS_WITH_AES_128_CBC_SHA

特别是,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA 在 API 11+ 的 list of supported protocols for Android 上。

我已验证服务器支持此功能

openssl s_client -connect server:port

返回

SSL-Session:
Protocol  : TLSv1.2
Cipher    : ECDHE-RSA-AES128-SHA

openssl 和 java 之间的名称略有不同,但 the openssl documentation 表示它们是相同的密码套件。

我的服务器支持并首先与与 Android 5.0 兼容的 openssl 客户端协商一个密码套件。我希望 Android 5.0 可以毫无问题地连接,但它失败了。

有没有人成功地将 Android 5.0 安全 websocket 连接连接到 Tomcat?是否有已知可以工作的密码套件?有没有办法调试 Android 客户端 SSL 实现?


更新

网络追踪结果:

SYN -->
<-- SYN, ACK
ACK -->
<-- Data
ACK -->
<-- certificates, SSL/TLS params? 1
<-- 2
<-- 3
<-- 4
ACK --> 
ACK --> 
ACK --> 
FIN(!), ACK --> 

当 Android 5.0 设备(Nexus 5)收到以 4-5 个数据包发送的服务器证书信息时,它会以可变数量 (2-4) 的 ACK 响应,然后是 FIN、ACK。在成功的跟踪中,客户端不发送 FIN。 Android 5 客户端不喜欢它从服务器获得的东西。

对于失败,服务器 SSL 调试信息说:

http-nio-8080-exec-10, called closeOutbound()
http-nio-8080-exec-10, closeOutboundInternal()
http-nio-8080-exec-10, SEND TLSv1.2 ALERT:  warning, description = close_notify
http-nio-8080-exec-10, WRITE: TLSv1.2 Alert, length = 2
[Raw write]: length = 7
0000: 15 03 03 00 02 01 00 

更新 2

这是一个简单的 Tyrus Android 应用程序

package edu.umd.mindlab.androidssldebug;

import android.support.v7.app.ActionBarActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.TextView;

import org.glassfish.tyrus.client.ClientManager;

import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.net.URI;

import javax.websocket.ClientEndpoint;
import javax.websocket.CloseReason;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;

@ClientEndpoint
public class MainActivity extends ActionBarActivity {
    public static final String TAG = "edu.umd.mindlab.androidssldebug";
    final Object annotatedClientEndpoint = this;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    @Override
    protected void onStart(){
        super.onStart();
        final Object annotatedClientEndpoint = this;
        new Thread(new Runnable(){
            @Override
            public void run() {
                try {
                    URI connectionURI = new URI("wss://mind7.cs.umd.edu:8080/test");
                    ClientManager client = ClientManager.createClient();
                    Object clientEndpoint = annotatedClientEndpoint;
                    client.connectToServer(clientEndpoint, connectionURI);
                }
                catch(Exception e){
                    ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
                    PrintStream printStream = new PrintStream(byteStream);
                    e.printStackTrace(printStream);
                    final String message = byteStream.toString();
                    Log.e(TAG, message);
                    e.printStackTrace();
                    runOnUiThread(new Runnable() {
                        public void run() {
                            TextView outputTextView = (TextView) findViewById(R.id.outputTextView);
                            outputTextView.setText(message);
                        }
                    });
                }
            }
        }).start();

    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            return true;
        }

        return super.onOptionsItemSelected(item);
    }

    @OnOpen
    public void onOpen(Session session) {
        Log.i(TAG, "opened");
        runOnUiThread(new Runnable() {
            public void run() {
                TextView outputTextView = (TextView) findViewById(R.id.outputTextView);
                outputTextView.setText("opened");
            }
        });

    }

    @OnMessage
    public void onMessage(String message, Session session) {
        Log.i(TAG, "message: " + message);
    }

    @OnClose
    public void onClose(Session session, CloseReason closeReason) {
        Log.i(TAG, "close: " + closeReason.toString() );
    }

    @OnError
    public void onError(Session session, Throwable t) {
        final String message = "error: " + t.toString();
        Log.e(TAG, message);
        runOnUiThread(new Runnable() {
            public void run() {
                TextView outputTextView = (TextView) findViewById(R.id.outputTextView);
                outputTextView.setText(message);
            }
        });
    }

}

【问题讨论】:

  • 请检查服务器日志中的错误消息以缩小问题范围。如果您没有找到任何东西,请对成功和不成功的连接进行数据包捕获并进行比较。如果您在解释数据包捕获方面需要帮助,请将它们发布到 cloudshark.org。
  • @mattm - 服务器使用的是什么版本的 OpenSSL?从终端尝试$ openssl version
  • OpenSSL 1.0.1e-fips 2013 年 2 月 11 日构建于:2014 年 11 月 6 日星期四 12:33:36 UTC
  • @jww 我有第二台服务器,其 OpenSSL 1.0.1j 2014 年 10 月 15 日显示相同的行为。
  • @mattm - 很抱歉提出这个问题(我无法使用 s_client 复制它)...我认为我们需要查看 PCAP。您能否在 Tomcat 服务器上收集一个用于与 Android 5.0 客户端的会话?

标签: ssl websocket android-5.0-lollipop grizzly tyrus


【解决方案1】:
error:1408E0F4:SSL routines:SSL3_GET_MESSAGE:unexpected message (external/openssl/ssl/s3_both.c:498 0xac526e61:0x00000000)
        at com.android.org.conscrypt.NativeCrypto.SSL_do_handshake_bio(Native Method)
        at com.android.org.conscrypt.OpenSSLEngineImpl.unwrap(OpenSSLEngineImpl.java:423)

0x1408E0F4 是:

$ openssl errstr 0x1408E0F4
error:1408E0F4:SSL routines:SSL3_GET_MESSAGE:unexpected message

它出现在几个地方的 OpenSSL 源代码中:

$ cd openssl-1.0.1l
$ grep -R SSL3_GET_MESSAGE *
ssl/s3_both.c:          SSLerr(SSL_F_SSL3_GET_MESSAGE,SSL_R_UNEXPECTED_MESSAGE);
ssl/s3_both.c:          SSLerr(SSL_F_SSL3_GET_MESSAGE,SSL_R_UNEXPECTED_MESSAGE);
ssl/s3_both.c:          SSLerr(SSL_F_SSL3_GET_MESSAGE,SSL_R_EXCESSIVE_MESSAGE_SIZE);
ssl/s3_both.c:          SSLerr(SSL_F_SSL3_GET_MESSAGE,SSL_R_EXCESSIVE_MESSAGE_SIZE);
ssl/s3_both.c:          SSLerr(SSL_F_SSL3_GET_MESSAGE,ERR_R_BUF_LIB);

这是我认为导致问题的代码(行号已更改,SSLerr 位于 491):

/* Obtain handshake message of message type 'mt' (any if mt == -1),
 * maximum acceptable body length 'max'.
 * The first four bytes (msg_type and length) are read in state 'st1',
 * the body is read in state 'stn'.
 */
long ssl3_get_message(SSL *s, int st1, int stn, int mt, long max, int *ok)
    {
    ...

    /* s->init_num == 4 */
    if ((mt >= 0) && (*p != mt))
        {
        al=SSL_AD_UNEXPECTED_MESSAGE;
        SSLerr(SSL_F_SSL3_GET_MESSAGE,SSL_R_UNEXPECTED_MESSAGE);
        goto f_err;
        }
    ...

但我不确定是什么导致了这个特定问题。在SSL_F_SSL3_GET_MESSAGE and SSL_R_UNEXPECTED_MESSAGE 的 OpenSSL 用户列表中查看此问题。

编辑:根据s3_both.c 的Android 源代码,这是触发问题的代码。

-----

好的,查看文件successful.pcapunsuccessful.pcap,好的客户端使用 TLS 1.0,而行为不端的客户端使用 TLS 1.2。但我没有看到任何会导致客户端在处理记录中的四个消息(服务器问候、证书、服务器密钥交换、服务器问候完成)时关闭连接的冒犯性行为。

-----

基于ServerKeyExchange消息:

服务器选择了客户端提供的 secp521r1。您可能想使用secp256。这是目前可互操作的。另见Is the limited elliptic curve support in rhel/centos/redhat openssl robust enough?

-----

服务器使用的 OpenSSL 1.0.1e FIPS 遇到了一些问题。参见,例如:

如果可能,您可能希望将其升级到更新版本。

-----

有没有办法调试 Android 客户端 SSL 实现?

我认为这是一个更简单的问题。使用自定义 SSLSocketFactory,例如 SSLSocketFactoryEx。它将允许您尝试不同的协议、密码套件和设置。但它的反复试验。

否则,您需要获取 Android 5.0 使用的 OpenSSL 源代码的副本(包括补丁)。我不知道如何获得它并确保它像主线 OpenSSL 一样构建(实际上,您需要使用 Android 源代码构建s_client 调试信息)。

这可能会有所帮助:OpenSSL on Android。从差异的外观来看,Android 似乎使用的是 OpenSSL 1.0.0。 (patch/ 目录中的一些补丁专门调用了 1.0.0b)。

【讨论】:

  • 此特定跟踪适用于 OpenSSL 1.0.1j。 192.168.0.109 是服务器,192.168.0.100 是客户端。发送第一个FIN时不是客户端在第14帧关闭连接吗?我在服务器配置中设置了 TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA 密码,因为它是 Android 4.4 和 Android 5 支持的密码的交集。我的第一个目标是让任何密码套件在 Android 5 和服务器之间成功协商。我更喜欢基于 SHA-256 或 SHA-384 的套件,但我没有找到任何成功协商任何密码的服务器配置。
  • @mattm - “不是客户端在第 14 帧关闭连接吗……” - 是的,我的错。我误读了哪一方发送了收盘价。
【解决方案2】:

确认这是由Android 5.0 bug 引起的。目前我不清楚 Tyrus websocket 或 Grizzly 是否也存在问题。

另请参阅:93740preview 328

【讨论】:

    【解决方案3】:

    TYRUS-402 的建议修复解决了这个问题。我打开了一个对应的灰熊虫GRIZZLY-1827,里面有对应的补丁。

    更新:错误GRIZZLY-1827 已修复。

    【讨论】:

    • 为了提高这个答案的质量,请总结您链接到的建议修复。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2018-07-28
    • 2012-10-11
    • 2021-12-28
    • 2018-10-17
    • 2016-04-29
    • 2014-03-05
    相关资源
    最近更新 更多