BouncyCastleJsseProvider: клиент вызвал фатальное (2) internal_error (80) предупреждение: не удалось прочитать запись

#java #ssl #bouncycastle #tls1.2

#java #ssl #bouncycastle #tls1.2

Вопрос:

Я столкнулся с проблемой при разработке HTTP-клиента с использованием библиотек BouncyCastle. Целевые версии (но ошибка также воспроизводится в Java 1.8.0_91 с той же версией BouncyCastle.)

  • JRE 1.6.0_45-b06
  • BouncyCastle jdk15to18 167 (bcprov-jdk15to18-167.jar , bcpkix-jdk15to18-167.jar , bctls-jdk15to18-167.jar )

Код HttpClient:

             String strURL = "https://www.<WEBSITE>.com";
            // CODE to set default TrustStore, KeyStore to be used

            // setup BC as SecurityProvider and SSLSocketFactoryProvider
/*
            Security.insertProviderAt(new BouncyCastleProvider(), 1);
            Security.insertProviderAt(new BouncyCastleJsseProvider(), 2);
            Security.setProperty("ssl.KeyManagerFactory.algorithm", "PKIX");
            Security.setProperty("ssl.TrustManagerFactory.algorithm", "PKIX");  
            Security.setProperty("ssl.SocketFactory.provider", "org.bouncycastle.jsse.provider.SSLSocketFactoryImpl");
            System.setProperty("jdk.tls.trustNameService", "true");
*/
            URL url = new URL( strURL );
            HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
            conn.setDoOutput(true);
            conn.setRequestMethod("GET");
            
            InputStream is = null;
            if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) {
                System.out.println("OK");
                is = conn.getInputStream();
            } else if (conn.getResponseCode() == HttpURLConnection.HTTP_INTERNAL_ERROR) {
                System.out.println("ERROR");
                is = conn.getErrorStream();
            } else  {
                System.out.println( conn.getResponseCode() );
                System.out.println( conn.getResponseMessage() );
            }
        
            if (is != null) {  
                System.out.println( readFullyAsString(is, "UTF-8") );
            }
            conn.disconnect();
 

Доступ к данным на веб-сайте с TLS 1.2 без client_auth работает нормально (есть некоторые проблемы с определенными сайтами, которые возвращают handshake_failure (40), но, к счастью, не в нашем случае). Но когда требуется client_auth, код завершается ошибкой со следующей (немного загадочной) ошибкой:

 nov. 27, 2020 5:23:10 PM org.bouncycastle.jsse.provider.ProvTlsClient notifyAlertRaised
WARNING: Client raised fatal(2) internal_error(80) alert: Failed to read record
java.net.SocketException: Connection reset
    at java.net.SocketInputStream.read(Unknown Source)
    at java.net.SocketInputStream.read(Unknown Source)
    at org.bouncycastle.tls.RecordStream$Record.fillTo(RecordStream.java:429)
    at org.bouncycastle.tls.RecordStream$Record.readHeader(RecordStream.java:468)
    at org.bouncycastle.tls.RecordStream.readRecord(RecordStream.java:201)
    at org.bouncycastle.tls.TlsProtocol.safeReadRecord(TlsProtocol.java:768)
    at org.bouncycastle.tls.TlsProtocol.readApplicationData(TlsProtocol.java:731)
    at org.bouncycastle.jsse.provider.ProvSSLSocketDirect$AppDataInput.read(ProvSSLSocketDirect.java:603)
    at java.io.BufferedInputStream.fill(Unknown Source)
    at java.io.BufferedInputStream.read1(Unknown Source)
    at java.io.BufferedInputStream.read(Unknown Source)
    at sun.net.www.http.HttpClient.parseHTTPHeader(Unknown Source)
    at sun.net.www.http.HttpClient.parseHTTP(Unknown Source)
    at sun.net.www.protocol.http.HttpURLConnection.getInputStream0(Unknown Source)
    at sun.net.www.protocol.http.HttpURLConnection.getInputStream(Unknown Source)
    at java.net.HttpURLConnection.getResponseCode(Unknown Source)
    at sun.net.www.protocol.https.HttpsURLConnectionImpl.getResponseCode(Unknown Source)
    at HttpsClient.main(HttpsClient.java:109)

nov. 27, 2020 5:23:10 PM org.bouncycastle.jsse.provider.ProvTlsClient notifyAlertRaised
WARNING: Client raised fatal(2) internal_error(80) alert: Failed to read record
java.net.SocketException: Connection reset
    at java.net.SocketInputStream.read(Unknown Source)
    at java.net.SocketInputStream.read(Unknown Source)
    at org.bouncycastle.tls.RecordStream$Record.fillTo(RecordStream.java:429)
    at org.bouncycastle.tls.RecordStream$Record.readHeader(RecordStream.java:468)
    at org.bouncycastle.tls.RecordStream.readRecord(RecordStream.java:201)
    at org.bouncycastle.tls.TlsProtocol.safeReadRecord(TlsProtocol.java:768)
    at org.bouncycastle.tls.TlsProtocol.readApplicationData(TlsProtocol.java:731)
    at org.bouncycastle.jsse.provider.ProvSSLSocketDirect$AppDataInput.read(ProvSSLSocketDirect.java:603)
    at java.io.BufferedInputStream.fill(Unknown Source)
    at java.io.BufferedInputStream.read1(Unknown Source)
    at java.io.BufferedInputStream.read(Unknown Source)
    at sun.net.www.http.HttpClient.parseHTTPHeader(Unknown Source)
    at sun.net.www.http.HttpClient.parseHTTP(Unknown Source)
    at sun.net.www.http.HttpClient.parseHTTP(Unknown Source)
    at sun.net.www.protocol.http.HttpURLConnection.getInputStream0(Unknown Source)
    at sun.net.www.protocol.http.HttpURLConnection.getInputStream(Unknown Source)
    at java.net.HttpURLConnection.getResponseCode(Unknown Source)
    at sun.net.www.protocol.https.HttpsURLConnectionImpl.getResponseCode(Unknown Source)
    at HttpsClient.main(HttpsClient.java:109)

java.net.SocketException: Connection reset
    at java.net.SocketInputStream.read(Unknown Source)
    at java.net.SocketInputStream.read(Unknown Source)
    at org.bouncycastle.tls.RecordStream$Record.fillTo(RecordStream.java:429)
    at org.bouncycastle.tls.RecordStream$Record.readHeader(RecordStream.java:468)
    at org.bouncycastle.tls.RecordStream.readRecord(RecordStream.java:201)
    at org.bouncycastle.tls.TlsProtocol.safeReadRecord(TlsProtocol.java:768)
    at org.bouncycastle.tls.TlsProtocol.readApplicationData(TlsProtocol.java:731)
    at org.bouncycastle.jsse.provider.ProvSSLSocketDirect$AppDataInput.read(ProvSSLSocketDirect.java:603)
    at java.io.BufferedInputStream.fill(Unknown Source)
    at java.io.BufferedInputStream.read1(Unknown Source)
    at java.io.BufferedInputStream.read(Unknown Source)
    at sun.net.www.http.HttpClient.parseHTTPHeader(Unknown Source)
    at sun.net.www.http.HttpClient.parseHTTP(Unknown Source)
    at sun.net.www.http.HttpClient.parseHTTP(Unknown Source)
    at sun.net.www.protocol.http.HttpURLConnection.getInputStream0(Unknown Source)
    at sun.net.www.protocol.http.HttpURLConnection.getInputStream(Unknown Source)
    at java.net.HttpURLConnection.getResponseCode(Unknown Source)
    at sun.net.www.protocol.https.HttpsURLConnectionImpl.getResponseCode(Unknown Source)
    at HttpsClient.main(HttpsClient.java:109)
 

Как и ожидалось, код отлично работает в Java 1.8 без BC в качестве поставщика безопасности… но в Java 1.6 BC необходим для поддержки TLS 1.2.

Я просмотрел пакеты с помощью Wireshark, но у меня недостаточно знаний, чтобы проанализировать, что происходит (сбой) с операцией установления связи SSL с сервером.

Чего мне здесь не хватает? Или это просто известное ограничение BC? Спасибо за любые предложения.

Комментарии:

1. 80 — это internal_error и может быть почти чем угодно, включая что-то совершенно законное. Лучший подход — выяснить из журналов сервера, в чем, по его мнению, проблема, возможно, после установки «подробного» или «отладочного» ведения журнала или аналогичного. Следующий шаг — выяснить, какую реализацию SSL / TLS и данные использует сервер, и реплицировать их в контролируемой среде. Последним было бы получить подробные сведения о TLS и просмотреть сообщения протокола Bouncy по сравнению со стандартным-JSSE и, возможно, другими рабочими клиентами, чтобы увидеть, что отличается, даже если это не незаконно или неправильно.

2. @dave_thompson_085 спасибо за ваши идеи: мы попытаемся воспроизвести его в нашей контролируемой среде, поскольку у нас нет доступа к серверу. Я опубликую наши результаты (и, надеюсь, решение)

Ответ №1:

После обширных исследований и тестирования (мы настроили нашу собственную среду разработки, которая реплицировала конечную точку, к которой мы пытались подключиться с помощью сертификата клиента), мы нашли решение (опубликовано ниже), которое работает для нас в нашей среде разработки, но не может быть настроено на конечной точке. Вот почему мы перешли на plan B — TLS termination proxy, и в нашем случае это сработало как чудо, когда у нас не было контроля над серверной средой.

Решение для IIS

В нашем случае веб-служба, требующая авторизации клиента, была запущена в Microsoft IIS. Как указано «К сожалению, наши библиотеки TLS не поддерживают повторное согласование (и мы не планируем его добавлять, хотя есть соответствующие функции TLS 1.3, которые мы будем)». в этой открытой проблеме (https://github.com/bcgit/bc-java/issues/593 ). После прочтения документации IIS и некоторого тестирования мы успешно настроили среду таким образом, чтобы сеансы TLS могли устанавливаться с помощью библиотеки BC TLS:

Шаг 1:

  • Создайте сайт IIS с настройками по умолчанию
  • Запустите IISCrypto с шаблоном «Лучшие практики»: ~ b390679
    ~ b433872
    ~ b338486

Шаг 2:

  • Включить SSLAlwaysNegoClientCert

Сохраните следующий текст в файл с именем «Enable_SSL_Renegotiate.js »

 var vdirObj=GetObject("IIS://localhost/W3svc/1");
// replace 1 on this line with the number of the web site you wish to configure
WScript.Echo("Value of SSLAlwaysNegoClientCert Before: "   vdirObj.SSLAlwaysNegoClientCert);
vdirObj.Put("SSLAlwaysNegoClientCert", true);
vdirObj.SetInfo();
WScript.Echo("Value of SSLAlwaysNegoClientCert After: "   vdirObj.SSLAlwaysNegoClientCert);
 

Выполните следующую команду из командной строки с повышенными правами / администратора:

 cscript.exe enable_ssl_renegotiate.js
 

Я опубликовал тот же ответ на BouncyCastle Github — # 847