SSLSocket через другой SSLSocket

Я пытаюсь создать SSLSocket поверх другого SSLSocket в приложении для Android. Нижнее соединение - это защищенное SSL-соединение с защищенным веб-прокси (HTTP-прокси по SSL), верхнее - по HTTP по SSL (HTTPS).

Для этого я использую SSLSocketFactory createSocket() функция, которая позволяет передавать существующий сокет, через который выполняется соединение SSL, например:

private Socket doSSLHandshake(Socket socket, String host, int port) throws IOException {
    TrustManager[] trustAllCerts = new TrustManager[]{
            new X509TrustManager(){
                public X509Certificate[] getAcceptedIssuers(){ return null; }
                public void checkClientTrusted(X509Certificate[] certs, String authType) {}
                public void checkServerTrusted(X509Certificate[] certs, String authType) {}
            }
    };

    try {
        SSLContext sslContext = SSLContext.getInstance("SSL");
        sslContext.init(null, trustAllCerts, new SecureRandom());
        SSLSocket sslSocket = (SSLSocket) sslContext.getSocketFactory().createSocket(socket, host, port, true);
        sslSocket.setEnabledProtocols(sslSocket.getSupportedProtocols());
        sslSocket.setEnableSessionCreation(true);
        sslSocket.startHandshake();
        return sslSocket;
    } catch (KeyManagementException | NoSuchAlgorithmException e) {
        throw new IOException("Could not do handshake: " + e);
    }
}

Этот код работает нормально, когда базовый сокет является обычным сокетом TCP, но когда я использую в качестве базового сокета SSLSocket, который был создан с использованием вышеупомянутого кода ранее, рукопожатие завершается неудачей со следующим исключением:

javax.net.ssl.SSLHandshakeException: Handshake failed
    at com.android.org.conscrypt.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:429)
    at com.myapp.MyThreadClass.doSSLHandshake(MyThreadClass.java:148)
    at com.myapp.MyThreadClass.run(MyThreadClass.java:254)
Caused by: javax.net.ssl.SSLProtocolException: SSL handshake aborted: ssl=0x7374d56e80: Failure in SSL library, usually a protocol error
    error:100000e3:SSL routines:OPENSSL_internal:UNKNOWN_ALERT_TYPE (external/boringssl/src/ssl/s3_pkt.c:618 0x738418ce7e:0x00000000)
    at com.android.org.conscrypt.NativeCrypto.SSL_do_handshake(Native Method)
    at com.android.org.conscrypt.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:357)
    ... 2 more

Я тестирую на Android 7.1.1. Приложение ориентировано на уровень SDK 23.

  • Что я могу делать не так?
  • Как я мог далее отладить проблему?
  • У кого-нибудь есть рабочий пример использования SSLSocket поверх другого SSLSocket в последней версии Android?

Любая помощь очень ценится!


Обновление: тот же код работает в JRE 1.8 на Mac, но не на Android.


Обновление 2: Концептуально, это шаги, через которые проходит соединение:

  1. Из приложения Android установите соединение с защищенным прокси-сервером (Socket)
  2. Выполните рукопожатие SSL/TLS с прокси-сервером (SSLSocket over Socket)
  3. Через SSLSocket отправьте сообщение CONNECT на прокси-сервер
  4. Прокси подключается к серверу назначения (https) и теперь копирует только байты
  5. Выполните рукопожатие SSL/TLS с сервером назначения (https) (SSLSocket через SSLSocket через Socket)
  6. Отправьте сообщение GET на сервер назначения и прочитайте ответ

Проблема возникает на шаге 5, когда выполняется рукопожатие на сокете SSLS, который проходит через сокет SSLS (который проходит через сокет).


Обновление 3: я сейчас открыл репозиторий GitHub с примером проекта и tcpdumps: https://github.com/FD-/SSLviaSSL


Примечание: я нашел и прочитал вопрос с очень похожим названием, но, к сожалению, он не содержал много полезной помощи.

4 ответа

Поэтому я попытался выяснить, что не так в случае с Android, но до сих пор я не нашел ничего плохого в вашем коде. Кроме того, поскольку код работает для JRE, он утверждает гипотезу.

Из предоставленного вами tcpdump вы найдете существенную информацию, позволяющую сделать вывод о том, как Android работает с тем же набором API, что и JRE.

Давайте посмотрим на JRE tcpdump:

  • См. Исходные сообщения о рукопожатии (Client Hello, Server Hello, изменение спецификации шифра). Это показывает рукопожатие между клиентом JRE и прокси-сервером. Это успешно.
  • Теперь мы не видим второго рукопожатия между клиентом JRE и www.google.com (конечным сервером), поскольку оно зашифровано, поскольку мы делаем SSL через SSL. Прокси-сервер постепенно копирует их на конечный сервер. Так что это правильное поведение.

Теперь давайте посмотрим на Android tcpdump:

  • См. Исходные сообщения о рукопожатии (Client Hello, Server Hello, изменение спецификации шифра). Это показывает рукопожатие между клиентом Android и прокси-сервером. Это успешно.
  • Теперь в идеале мы не должны видеть второе рукопожатие, поскольку оно должно быть зашифровано. Но здесь мы видим, что клиент android отправляет "hello client" и отправляет его на "www.google.com", хотя пакет отправляется на прокси-сервер.
  • Вышеприведенное обязательно приведет к сбою, поскольку пакет должен был быть записан через сокет SSL, а не поверх исходного простого сокета. Я просмотрел ваш код и вижу, что вы выполняете второе рукопожатие через SSLSocket, а не через обычный сокет.

Анализ от прокси /stunnel wireshark:

Дело JRE:

В случае JRE клиент выполняет начальное рукопожатие SSL с stunnel / proxy-сервером. То же самое можно увидеть ниже:

Рукопожатие прошло успешно, и соединение установлено

Затем клиент пытается подключиться к удаленному серверу (www.google.com) и запускает рукопожатие. Таким образом, клиентское приветствие, отправленное клиентом, рассматривается как зашифрованное сообщение в пакете № 34, и когда stunnel дешифрует то же самое, оно отображается в "Client hello", которое перенаправляется на прокси-сервер с помощью stunnel.

Теперь давайте посмотрим на случай клиента Android.

Первоначальное рукопожатие SSL от клиента к stunnel / proxy прошло успешно, как показано выше.

Затем, когда Android-клиент запускает рукопожатие с удаленным (www.google.com), в идеале он должен использовать SSL-сокет для того же. Если это так, мы должны увидеть зашифрованный трафик с Android на Stunnel (аналогично пакету № 34 из JRE), Stunnel должен расшифровать и отправить "hello client" на прокси. однако, как вы можете видеть ниже, клиент-андроид посылает "привет клиенту" через обычный сокет.

Если вы сравните пакет № 24 с пакетом № 34 от JRE, мы можем заметить эту разницу.

Заключение:

Это ошибка с Android-SSL (factory.createsocket() с реализацией сокета SSL), и я чувствую, что не может быть волшебного обходного пути для одного и того же набора API. На самом деле я нашел эту проблему в списке ошибок Android. См. Ссылку ниже: https://code.google.com/p/android/issues/detail?id=204159

Эта проблема до сих пор не решена, и вы, вероятно, можете продолжить с командой разработчиков Android, чтобы исправить то же самое.

Возможные решения:

Если мы придем к выводу, что один и тот же набор API не может работать, у вас остается только одна опция:

  1. Напишите свою собственную оболочку SSL через сокет SSL. Вы можете вручную сделать рукопожатие или использовать стороннюю реализацию. Это может занять некоторое время, но выглядит как единственный способ.

Я не думаю, что вы делаете что-то не так. Похоже, что во время вашего второго рукопожатия произошла ошибка при согласовании протокола. Хороший кандидат может потерпеть неудачу в расширении рукопожатия TLS NPN.

Посмотрите на ваши протоколы в этом звонке: sslSocket.setEnabledProtocols(sslSocket.getSupportedProtocols());

Вы можете пройти через перечисленные протоколы и попробовать их индивидуально. Посмотрите, можете ли вы заблокировать то, что не работает, и нужен ли вам конкретный протокол или расширение.

Поскольку HTTPS не должен допускать, чтобы посредник прерывал связь между ними. вот почему вы не можете это сделать. так что второе рукопожатие терпит неудачу.

Вот ссылка может помочь.

HTTPS-соединения через прокси-серверы

Я не знаю, помогает ли это, но:
Я создал вашу среду тестирования репо, и моя ошибка немного отличается:

I/SurfaceTextureClient(20733): [0x52851b98] frames:2, duration:1.005000, fps:1.989805
I/System.out(20733): [socket][2] connection /192.168.1.100:10443;LocalPort=35380(0)
I/System.out(20733): [CDS]connect[/192.168.1.100:10443] tm:90
I/System.out(20733): [socket][/192.168.1.123:35380] connected
I/System.out(20733): Doing SSL handshake with 192.168.1.100:10443
I/System.out(20733): Supported protocols are: [SSLv3, TLSv1, TLSv1.1, TLSv1.2]
E/NativeCrypto(20733): ssl=0x53c96268 cert_verify_callback x509_store_ctx=0x542e0a80 arg=0x0
E/NativeCrypto(20733): ssl=0x53c96268 cert_verify_callback calling verifyCertificateChain authMethod=RSA
I/System.out(20733): Doing SSL handshake with 192.168.1.100:443
I/System.out(20733): Supported protocols are: [SSLv3, TLSv1, TLSv1.1, TLSv1.2]
E/NativeCrypto(20733): Unknown error during handshake
I/System.out(20733): Shutdown rx/tx
I/System.out(20733): [CDS]close[35380]
I/System.out(20733): close [socket][/0.0.0.0:35380]
W/System.err(20733): javax.net.ssl.SSLHandshakeException: javax.net.ssl.SSLProtocolException:
                        SSL handshake aborted: ssl=0x53c9c1d8:
                        Failure in SSL library, usually a protocol error
W/System.err(20733): error:140770FC:SSL routines:
                        SSL23_GET_SERVER_HELLO:
                        unknown protocol (external/openssl/ssl/s23_clnt.c:766 0x4e7cb3ad:0x00000000)
W/System.err(20733):    at org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:413)
W/System.err(20733):    at com.bugreport.sslviassl.SecureWebProxyThread.doSSLHandshake(SecureWebProxyThread.java:147)
W/System.err(20733):    at com.bugreport.sslviassl.SecureWebProxyThread.run(SecureWebProxyThread.java:216)
W/System.err(20733): Caused by: javax.net.ssl.SSLProtocolException:
                        SSL handshake aborted: ssl=0x53c9c1d8:
                        Failure in SSL library, usually a protocol error
W/System.err(20733): error:140770FC:SSL routines:
    SSL23_GET_SERVER_HELLO:unknown protocol (external/openssl/ssl/s23_clnt.c:766 0x4e7cb3ad:0x00000000)
W/System.err(20733):    at org.apache.harmony.xnet.provider.jsse.NativeCrypto.SSL_do_handshake(Native Method)
W/System.err(20733):    at org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:372)
W/System.err(20733):    ... 2 more
I/SurfaceTextureClient(20733): [0x52851b98] frames:5, duration:1.010000, fps:4.946089

Одна мысль о потоках: в каком потоке вы используете свой метод doSSLHandshake()?

public void policy()
{
    int SDK_INT = android.os.Build.VERSION.SDK_INT;
    if (SDK_INT > 8)
    {
        StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build();
        StrictMode.setThreadPolicy(policy);
    }
}

Это действительно оказалось ошибкой в ​​поставщиках SSL по умолчанию на любом устройстве Android, которое я тестировал (хотя мое новейшее устройство - Nexus 9 под управлением Android 7.1.1). В конце концов, я обнаружил, что реализация SSLSocket на основе движка (недавно выпущенная) автономной версии провайдера Conscrypt SSL работает на Android именно так, как я ожидал.

Для получения дополнительной информации ознакомьтесь с обсуждением с сопровождающими Conscrypt на GitHub: https://github.com/google/conscrypt/issues/104

Другие вопросы по тегам