Клиент C с сервером OpenSSL + Java: javax.net.ssl.SSLHandshakeException: нет общих наборов шифров

У меня есть сокет Java SSL и клиент c с OpenSSL (клиенты Java нормально работают с этим сервером Java). Сбой рукопожатия, и я получаю исключение Java:

javax.net.ssl.SSLHandshakeException: no cipher suites in common
    at sun.security.ssl.Alerts.getSSLException(Alerts.java:192)
    at sun.security.ssl.SSLSocketImpl.fatal(SSLSocketImpl.java:1904)
    at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:279)
    at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:269)
    at sun.security.ssl.ServerHandshaker.chooseCipherSuite(ServerHandshaker.java:901)
    at sun.security.ssl.ServerHandshaker.clientHello(ServerHandshaker.java:629)
    at sun.security.ssl.ServerHandshaker.processMessage(ServerHandshaker.java:167)
    at sun.security.ssl.Handshaker.processLoop(Handshaker.java:901)
    at sun.security.ssl.Handshaker.process_record(Handshaker.java:837)
    at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1023)
    at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1332)
    at sun.security.ssl.SSLSocketImpl.readDataRecord(SSLSocketImpl.java:889)
    at sun.security.ssl.AppInputStream.read(AppInputStream.java:102)
    at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:283)
    at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:325)
    at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:177)
    at java.io.InputStreamReader.read(InputStreamReader.java:184)
    at java.io.BufferedReader.fill(BufferedReader.java:154)
    at java.io.BufferedReader.readLine(BufferedReader.java:317)
    at java.io.BufferedReader.readLine(BufferedReader.java:382)
    at EchoServer.main(EchoServer.java:36)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:606)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:134)

Вот как создается SSL-сокет сервера:

public class EchoServer {
    public static void main(String[] arstring) {
        try {
            final KeyStore keyStore = KeyStore.getInstance("JKS");

            final InputStream is = new FileInputStream("/Path/mySrvKeystore.jks");
            keyStore.load(is, "123456".toCharArray());
            final KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory .getDefaultAlgorithm());
            kmf.init(keyStore, "123456".toCharArray());
            final TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory .getDefaultAlgorithm());
            tmf.init(keyStore);

            SSLContext sc = SSLContext.getInstance("TLSv1.2");
            sc.init(kmf.getKeyManagers(), tmf.getTrustManagers(), new java.security.SecureRandom());

            SSLServerSocketFactory sslserversocketfactory = (SSLServerSocketFactory) SSLServerSocketFactory.getDefault();
            SSLServerSocket sslserversocket = (SSLServerSocket) sslserversocketfactory.createServerSocket(9997);
            SSLSocket sslsocket = (SSLSocket) sslserversocket.accept();
            sslsocket.setEnabledCipherSuites(sc.getServerSocketFactory().getSupportedCipherSuites());

            InputStream inputstream = sslsocket.getInputStream();
            InputStreamReader inputstreamreader = new InputStreamReader(inputstream);
            BufferedReader bufferedreader = new BufferedReader(inputstreamreader);

            String string = null;
            while ((string = bufferedreader.readLine()) != null) {
                System.out.println(string);
                System.out.flush();
            }
        } catch (Exception exception) {
            exception.printStackTrace();
        }
    }
}

С-клиент:

BIO *certbio = NULL;
BIO *outbio = NULL;
SSL_METHOD *ssl_method;
SSL_CTX *ssl_ctx;
SSL *ssl;

int sd;

// These function calls initialize openssl for correct work.
OpenSSL_add_all_algorithms();
ERR_load_BIO_strings();
ERR_load_crypto_strings();
SSL_load_error_strings();

// Create the Input/Output BIO's.
certbio = BIO_new(BIO_s_file());
outbio  = BIO_new_fp(stdout, BIO_NOCLOSE);

// initialize SSL library and register algorithms
if(SSL_library_init() < 0)
    BIO_printf(outbio, "Could not initialize the OpenSSL library !\n");

ssl_method = (SSL_METHOD*)TLSv1_2_method();

// Try to create a new SSL context
if ( (ssl_ctx = SSL_CTX_new(ssl_method)) == NULL)
    BIO_printf(outbio, "Unable to create a new SSL context structure.\n");

// flags
SSL_CTX_set_options(ssl_ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_NO_COMPRESSION | SSL_OP_CIPHER_SERVER_PREFERENCE);

// Create new SSL connection state object
ssl = SSL_new(ssl_ctx);

// Make the underlying TCP socket connection
struct sockaddr_in address;

memset(&address, 0, sizeof(address));
address.sin_family = AF_INET;
address.sin_port = htons(port);

const char *dest_url = this->host.c_str();

address.sin_addr.s_addr = inet_addr(dest_url);
address.sin_port = htons(port);

sd = socket(AF_INET, SOCK_STREAM, 0);
int connect_result = ::connect(sd, (struct sockaddr*)&address, sizeof(address));
if (connect_result != 0) {
    BIO_printf(outbio, "Failed to connect over TCP with error %i\n", connect_result);
    throw IOException("Connection refused");
} else {
    BIO_printf(outbio, "Successfully made the TCP connection to: %s:%i\n", dest_url, port);
}

// Attach the SSL session to the socket descriptor
SSL_set_fd(ssl, sd);

// Try to SSL-connect here, returns 1 for success
int ssl_connect_result = SSL_connect(ssl);
if (ssl_connect_result != 1)
    BIO_printf(outbio, "Error: Could not build a SSL session to: %s:%i with error %i\n", dest_url, port, ssl_connect_result);
else
    BIO_printf(outbio, "Successfully enabled SSL/TLS session to: %s\n", dest_url);

Вот вывод на стороне клиента:

Ошибка: не удалось создать сеанс SSL для: 127.0.0.1:9997 с ошибкой -1

Обновление 1

int ssl_connect_result = SSL_connect(ssl);
if (ssl_connect_result != 1) {
    int error_code = SSL_get_error(ssl, ssl_connect_result); // =1
    BIO_printf(outbio, "Error: Could not build a SSL session to: %s:%i with error %i (%i)\n", dest_url, port, ssl_connect_result, error_code);
} else {
    BIO_printf(outbio, "Successfully enabled SSL/TLS session to: %s\n", dest_url);
}

И вывод:

Ошибка: не удалось создать сеанс SSL для: 127.0.0.1:9997 с ошибкой -1 (1)

Обновление 2

Я забыл отметить, что я использую самозаверяющий сертификат, сгенерированный keytool из JDK.

Обновление 3

Я заметил, что пропустил несколько строк, и добавил:

OpenSSL_add_all_ciphers();
OpenSSL_add_all_digests();

но все равно не повезло - получаю ту же ошибку -1.

Обновление 4

Вот клиент Java, который принимается приведенным выше кодом сервера:

SSLSocketFactory sslsocketfactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
SSLSocket sslsocket = (SSLSocket) sslsocketfactory.createSocket(ip, port);
sslsocket.setEnabledCipherSuites(sslsocketfactory.getSupportedCipherSuites());

InputStream inputstream = System.in;
InputStreamReader inputstreamreader = new InputStreamReader(inputstream);
BufferedReader bufferedreader = new BufferedReader(inputstreamreader);

OutputStream outputstream = sslsocket.getOutputStream();
OutputStreamWriter outputstreamwriter = new OutputStreamWriter(outputstream);
String string = null;

outputstreamwriter.write("hello");
outputstreamwriter.flush();

while ((string = bufferedreader.readLine()) != null) {
    outputstreamwriter.write(string);
    outputstreamwriter.flush();
}
sslsocket.close();

Я проверил, что не вижу простых данных в пакетах, перехваченных в сети, поэтому он выполняет некоторое шифрование данных.


person 4ntoine    schedule 18.12.2015    source источник
comment
Вам нужно позвонить SSL_get_error() после SSL_connect(), чтобы узнать причину вашей проблемы. Я действительно не помню никаких проблем с блокирующими сокетами, но с неблокирующими вам приходилось вызывать SSL_connect() много раз, чтобы он мог выполнять множество операций записи и чтения. Точнее, пока ошибка была SSL_want_read или SSL_want_write. О, и вы забыли SSL_set_connect_state(), так что OpenSSL знает, что вы используете здесь клиента.   -  person Leśny Rumcajs    schedule 18.12.2015
comment
согласно документу(openssl.org/docs/manmaster/ssl/SSL_set_connect_state.html) При использовании процедур SSL_connect или SSL_accept правильные процедуры рукопожатия устанавливаются автоматически. Должен ли я вызывать SSL_set_connect_state , даже если я вызываю SSL_connect? Я постараюсь получить больше информации, используя SSL_get_error   -  person 4ntoine    schedule 18.12.2015
comment
Обновлен вопрос с результатом SSL_get_error , который равен 1 (SSL_ERROR_SSL).   -  person 4ntoine    schedule 18.12.2015
comment
Избавьтесь от вызова setEnabledCipherSuites(). Небезопасно использовать все поддерживаемые наборы шифров.   -  person user207421    schedule 19.12.2015
comment
Пробовали ли вы подключить свой C-клиент к образцу сервера OpenSSL? Используя openssl s_server ? С требованием сертификата и без? Это может оказаться очень полезным, особенно с -msg и -debug.   -  person Leśny Rumcajs    schedule 19.12.2015


Ответы (2)


Я не верю, что сервер Java также принимает клиента Java, если только клиент Java не делает то же самое .setEnabledCipherSuites (all-supported) -- если это так, он использует анонимный (не прошедший проверку подлинности) набор шифров, который не защищен от активной атаки, и хотя многие люди все еще застряли в модели пассивных угроз примерно 1980 года, сегодня активные атаки являются обычным явлением. Вот почему список шифров JSSE по умолчанию исключает анонимные шифры, если только вы не переопределите его. И почему список шифров OpenSSL по умолчанию также исключает их, что вы не переопределили.

(добавить) Чтобы объяснить проще, анонимные наборы шифров ЗАШИФРОВАНЫ (за некоторыми исключениями, которые здесь неуместны, поскольку они никогда не являются предпочтительными), но НЕ аутентифицированы. Слово «неаутентифицированный» означает «неаутентифицированный»; это не означает «не зашифровано». Слово «незашифрованный» используется для обозначения «не зашифрованного». «Не зашифровано» означает, что что-то, что просто просматривает канал, например Wireshark, может видеть открытый текст. «Неаутентифицированный» означает, что злоумышленник, который перехватывает (возможно, перенаправляет) ваш трафик, может заставить вас установить «безопасный» сеанс с злоумышленником посередине, и они могут расшифровать ваши данные, скопировать и/или изменить их по своему усмотрению, повторно -зашифруйте его и отправьте дальше, и вы будете думать, что это правильно и конфиденциально, когда это не так. Погуглите или поищите здесь (думаю, в основном это security.SE и суперпользователь) такие вещи, как «атака человека посередине», «обман ARP», «обман MAC», «отравление DNS», «атака BGP» и т. д.

Непосредственная проблема заключается в том, что вы не используете хранилище ключей. Вы создаете SSLContext с ключом и менеджерами доверия из него, но затем вы создаете сокет из SSLServerSocketFactory.getDefault(), который не использует контекст. Вместо этого используйте sc.getServerSocketFactory().

(добавьте) почему? Каждый SSLSocketSSLServerSocket) связан с SSLContext, который, среди прочего, управляет закрытым ключом (-ами) и используемыми сертификатами или цепочками, а также доверенными сертификатами. (Соединения SSL/TLS обычно аутентифицируют только сервер, поэтому на практике серверу нужен только ключ и цепочка, а клиенту нужен только корневой сертификат, но Java использует один и тот же формат файла хранилища ключей для обоих, и его легко просто закодировать оба.) Поскольку ваш код установил конкретный SSLContext sc для содержания подходящего ключа и сертификата, sc.getServerSocketFactory() создает фабрику, которая создает SSLServerSocket, которая, в свою очередь, создает SSLSocket (для каждого соединения, если их более одного), который использует этот ключ-и -(пока это позволяет поддерживаемый клиентом список шифров, и здесь это разрешено).

(добавить) SSLServerSocketFactory.getDefault() создает фабрику и, следовательно, сокеты, используя контекст SSL по умолчанию, который по умолчанию содержит БЕЗ цепочки ключей, хотя вы можете изменить это с помощью системных свойств, как описано в документации, искусно скрытой по адресу http://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/JSSERefGuide.html . В результате он не может согласовать аутентифицированный набор шифров. Поскольку и Java, и OpenSSL по умолчанию отключают наборы шифров без проверки подлинности, вот почему вы не получаете «общих наборов шифров», если только вы .setEnabled не включаете наборы шифров без проверки подлинности и небезопасные наборы шифров в Java и по-прежнему получаете их для клиента OpenSSL, поскольку вы ничего не сделали. чтобы включить там неаутентифицированные и небезопасные наборы шифров.

(добавить) Если вы внимательно посмотрите на свою трассировку Wireshark, вы увидите в ServerHello, что выбранный набор шифров использует обмен ключами DH_anon или ECDH_anon -- "anon" является аббревиатурой от "anonymous", что означает " не аутентифицирован», как объяснялось выше, и нет сообщения Certificate от сервера, и (менее очевидно, если вы не знаете об этом) данные ServerKeyExchange не подписаны.
Также я предполагаю, что ваш Java-клиент будет проверять sslsocket.getSession().getCipherSuite() и/или sslsocket.getSession().getPeerCertificates() после выполнения рукопожатия, что, поскольку вы не сделаете это явно, будет на первом вводе-выводе на уровне сокета, который будет outputstreamwriter.flush(), вы увидите анонимный набор шифров и отсутствие сертификата партнера (он выдает PeerNotAuthenticated).

Другие пункты:

(1) Как правило, всякий раз, когда вы получаете SSL_ERROR из SSL_get_error() или какую-либо ошибку возвращают процедуры более низкого уровня, такие как EVP_* и BIO_*, вы должны использовать процедуры ERR_* для получения сведений об ошибке и регистрации/отображения их; см. https://www.openssl.org/docs/faq.html#PROG6 и https://www.openssl.org/docs/manmaster/crypto/ERR_print_errors.html и друзья. Тем более, что вы уже загрузили строки ошибок, тем самым избегая https://www.openssl.org/docs/faq.html#PROG7 . Однако в этом случае вы уже достаточно знаете со стороны сервера, поэтому детали со стороны клиента не нужны.

(2) Вам не нужны _add_all_ciphers и _add_all_digests, они включены в _add_all_algorithms.

(3) OP_NO_SSLv2 и 3 не влияют на TLSv1_2_method, а OP_SERVER_CIPHER_PREFERENCE не влияют на клиента. (Они не причиняют вреда, они просто бесполезны и, возможно, сбивают с толку.)

(4) После согласования шифра клиенту OpenSSL потребуется корневой сертификат для сервера; поскольку вы собираетесь использовать самозаверяющий сертификат (после того, как вы исправите сервер для использования хранилища ключей вообще), этот сертификат является его собственным корнем. В 1.0.2 (не ранее) вы также могли использовать якорь доверия без полномочий root, но не по умолчанию, и у вас его все равно нет. Я предполагаю, что certbio был предназначен для этого, но вы никогда не открываете его в реальном файле и не делаете с ним что-либо еще, и в любом случае библиотека SSL не может использовать BIO для своего хранилища доверия. У вас есть три варианта:

  • поместите сертификат(ы) в файл или каталог, используя специальные хеш-имена, и передайте имя(я) файла и/или каталога в SSL_CTX_load_verify_locations. Если вам нужен только один корень (ваш собственный), проще использовать опцию CAfile.

  • поместите или добавьте сертификаты в файл по умолчанию или хешированный каталог, определенный вашей компиляцией библиотеки OpenSSL, и вызовите SSL_CTX_set_default_verify_paths; обычно это что-то вроде /etc/pki или /var/ssl. Если вы хотите использовать один и тот же сертификат (сертификаты) для нескольких программ или для командной строки openssl, это общее расположение обычно проще.

  • используйте BIO и/или другие средства для (открытия и) чтения сертификатов в память, создайте собственный X509_STORE, содержащий их, и поместите его в свой SSL_CTX. Это более сложно, поэтому я не буду распространяться об этом, если вы этого не хотите.

(5) Ваш dest_url является (по крайней мере, в этом случае?) адресной строкой, а не URL-адресом; это разные, хотя и связанные вещи, и думать, что они одинаковы, вызовет у вас гораздо больше проблем. Для большинства программ лучше обрабатывать строку name хоста с помощью классического gethostbyname и вернуться к inet_addr или, что лучше, к «новому» (с 1990-х годов) getaddrinfo, который может обрабатывать как строки имени, так и строки адреса, а также IPv4. и v6 (также новый с 1990-х годов, но, наконец, набирающий обороты). По крайней мере, вы должны проверить, что inet_addr возвращает INADDR_NONE, указывающее, что это не удалось.

person dave_thompson_085    schedule 18.12.2015
comment
Во-первых, спасибо за такой подробный ответ. Мои комментарии таковы: 1) Java lient действительно может подключаться к этому сокету Java SSL, и я не вижу простых данных в пакетах. 2) я проверил и могу подтвердить, что sc.getServerSocketFactory() и SSLServerSocketFactory.getDefault() возвращают разные объекты. Какая разница? Я считаю, что sc.get .. должен возвращать фабрику, которая на самом деле выполняет шифрование, поэтому почему SSLServerSocketFactory.getDefault() существует в мире, поскольку каждое соединение относится к некоторому контексту SSL, а default нет? 3) Я собираюсь проверить другие пункты, которые вы упомянули. Спасибо - person 4ntoine; 19.12.2015
comment
я прокомментировал setEnabledCipherSuits как на Java-сервере, так и на клиенте, и это больше не может общаться (javax.net.ssl.SSLHandshakeException: no cipher suites in common на стороне сервера и javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure на стороне клиента. И да - я создаю фабрику серверных сокетов, используя sc.getServerSocketFactory() на стороне сервера). Что случилось? После этого я перейду на клиент c - person 4ntoine; 19.12.2015

SSLContext sc = SSLContext.getInstance("TLSv1.2");

Раньше Java делала две вещи с кодом, подобным этому:

  • включить SSLv3
  • отключить TLS 1.1 и 1.2

... Даже если вы вызвали TLS. По сути, все, что вы могли получить, — это SSLContext с SSLv3 и TLS 1.0 (если только вы не хотели выполнять дополнительную работу). Возможно, это уже не так, но это объясняет ошибку, которую вы видите (особенно если вы используете Java 7 или 8).

Вам нужно проделать больше работы в Java, чтобы получить «TLS 1.0 или выше» и «только TLS 1.2». Для этого см. Какие наборы шифров включить для сокета SSL?. Он показывает вам, как включать/отключать как протоколы, так и наборы шифров.


SSL_CTX_set_options(ssl_ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_NO_COMPRESSION | SSL_OP_CIPHER_SERVER_PREFERENCE);

Вы также должны установить список наборов шифров, поскольку OpenSSL по умолчанию включает сломанные/слабые/раненые шифры. Что-то типа:

const char PREFERRED_CIPHERS[] = "HIGH:!aNULL:!kRSA:!PSK:!SRP:!MD5:!RC4";
long res = SSL_CTX_set_cipher_list(ssl_ctx, PREFERRED_CIPHERS);
ASSERT(res == 1);

Я думаю, что ваш последний шаг — убедиться, что самозаверяющий сертификат является доверенным.

Для Java-клиента см. Как правильно импортировать самозаверяющий сертификат в хранилище ключей Java, которое по умолчанию доступно для всех приложений Java? . Для клиента OpenSSL просто загрузите его, используя SSL_CTX_load_verify_locations, см. SSL_CTX_load_verify_locations Сбой с SSL_ERROR_NONE.


Обратите внимание: OpenSSL до версии 1.1.0 не выполнял проверку имени хоста. Вам придется сделать это самостоятельно. Если вам нужно выполнить проверку имени хоста, поднимите код из cURL's url.c. Кажется, я припоминаю, что смотрел на код Дэниела, и он очень солидный. Вы также можете получить код из проекта Google CT. . Я никогда не проверял его, поэтому я не знаю, каково это.

Есть еще две проверки, которые вы должны сделать; они обсуждаются в разделе SSL/TLS Client на вики OpenSSL.

person jww    schedule 21.12.2015
comment
В какой версии Java это «использовалось»? - person user207421; 21.12.2015
comment
@EJP - 6,7 и 8. Я перестал отправлять отчеты об ошибках в Oracle по этой проблеме, потому что они не исправляли их между выпусками. - person jww; 21.12.2015
comment
я тестирую JDK 1.6 на Mac. Я знаю, что были выпущены версии 7 и 8, но мне не нужны новые ошибки и несовместимости. - person 4ntoine; 21.12.2015
comment
@ 4ntoine - О, в таком случае ... Я не думаю, что TLS 1.2 тогда поддерживался. Он вообще указан как доступный протокол (например, getSupportedProtocols())? Если это так, то у вас могут возникнуть проблемы с безопасным повторным согласованием и несколькими другими больными местами SSL/TLS. См. также Инициализация SSLContext. - person jww; 21.12.2015
comment
да, TLSv1.2 указан в списке поддерживаемых протоколов. проверю по ссылке выше - person 4ntoine; 21.12.2015