使用OpenSSL + Java服务器的C客户端:javax.net.ssl.SSLHandshakeException:没有共同的密码套件

时间:2015-12-18 14:44:06

标签: java ssl openssl jsse tls1.2

我有Java SSL套接字和带有OpenSSL的c客户端(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();
        }
    }
}

C客户:

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

我忘了注意我使用的是由JDK keytool生成的自签名证书。

更新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();

我已经检查过我无法在网络中截获的数据包中看到普通数据,因此它会执行一些数据加密。

1 个答案:

答案 0 :(得分:3)

我不相信Java服务器也会接受Java客户端,除非Java客户端同样.setEnabledCipherSuites (all-supported) - 如果是这样的话,它使用的匿名(未经身份验证的)密码套件对于主动攻击是不安全的尽管许多人仍然陷入大约1980年的被动威胁模型,但今天的主动攻击很常见。这就是为什么JSSE的默认密码列表不包括匿名密码 - 除非你覆盖它。为什么OpenSSL的默认密码列表也排除了它们 - 你没有覆盖它们。

(添加)用较小的词来解释,匿名密码套件是加密的(这里有一些例外,因为它们永远不是首选,因此不相关)但未经过身份验证。单词&#34; unauthenticated&#34;意味着&#34;未经过身份验证&#34 ;;它并不意味着&#34;没有加密&#34;。单词&#34;未加密&#34;用于表示&#34;未加密&#34;。 &#34;未加密&#34;意味着只看通道的东西,比如Wireshark,可以看到明文。 &#34;未经过身份验证&#34;意味着拦截(可能会转移)您的流量的攻击者可能会导致您建立“安全”#34;与攻击者在中间进行会话,他们可以解密您的数据,按照自己的意愿复制和/或更改数据,重新加密并发送,如果不是这样的话,您会认为它是正确且私密的。吨。谷歌或在这里搜索(我认为主要是安全。超级用户),例如&#34;中间人攻击&#34;,&#34; ARP欺骗&#34;,&#34; MAC恶搞&#34;, &#34; DNS中毒&#34;,&#34; BGP攻击&#34;等

当前的问题是您没有使用密钥库。您可以使用密钥和信任管理器创建SSLContext,然后从SSLServerSocketFactory.getDefault()创建不使用上下文的套接字。 改为使用sc.getServerSocketFactory()

(添加)为什么?每个SSLSocket(和SSLServerSocket)都链接到SSLContext,其中包括私钥和证书(s)或使用的链和证书。 (SSL / TLS连接通常只对服务器进行身份验证,因此实际上服务器只需要一个密钥和链,而客户端只需要根证书,但Java对两者都使用相同的密钥库文件格式。很容易只编码两者。)由于您的代码已将特定SSLContext sc设置为包含合适的密钥和证书,sc.getServerSocketFactory()创建一个工厂,创建SSLServerSocket,然后创建一个SSLSocket(对于每个连接,如果多于一个)使用该密钥 - 并且(只要客户端支持的密码列表允许它,并且在这里它就可以了。)

(添加) SSLServerSocketFactory.getDefault()使用默认 SSL上下文创建工厂,从而创建套接字,默认情况下包含 NO键 - 和-chain ,尽管您可以使用系统属性对其进行更改,如http://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/JSSERefGuide.html中巧妙隐藏的文档中所述。因此,它无法协商经过身份验证的密码套件。由于默认情况下Java和OpenSSL都禁用了未经身份验证的密码套件,因此这就是为什么你没有共同的密码套件&#34;除非你.setEnabled在Java中包含未经身份验证和不安全的密码套件,并且仍然为OpenSSL客户端提供它,因为你没有做任何事情来启用那里未经验证和不安全的密码套件。

(添加)如果仔细查看Wireshark跟踪,您会在ServerHello中看到所选密码套件使用DH_anonECDH_anon密钥交换 - &#34; anon&#34;是&#34; anonymous&#34;的缩写。这意味着&#34;未经过身份验证&#34;如上所述 - 并且服务器没有Certificate消息,并且(除非您知道,否则不太明显)ServerKeyExchange数据未签名。
另外我预测在握手完成后你是否让你的Java客户端检查sslsocket.getSession().getCipherSuite()和/或sslsocket.getSession().getPeerCertificates(),因为你没有明确地将它放在第一个套接字上-level I / O将是outputstreamwriter.flush(),您将看到匿名密码套件,并且没有对等证书(它会抛出PeerNotAuthenticated)。

其他要点:

(1)通常,只要您从SSL_ERROR获得SSL_get_error(),或者从EVP_*BIO_*这样的较低级别例程返回任何错误,就应该使用{{ 1}}例程来获取错误的详细信息并记录/显示它们;见https://www.openssl.org/docs/faq.html#PROG6https://www.openssl.org/docs/manmaster/crypto/ERR_print_errors.html et amici。特别是因为你已经加载了错误字符串,因此避免了https://www.openssl.org/docs/faq.html#PROG7。在这种情况下,您已经从服务器端了解了足够多,因此不需要客户端详细信息。

(2)您不需要ERR_*_add_all_ciphers,它们包含在_add_all_digests中。

(3)_add_all_algorithmsOP_NO_SSLv23没有影响,而TLSv1_2_method对客户端没有影响。 (他们没有伤害,他们只是无用而且可能令人困惑。)

(4)一旦通过密码协商,OpenSSL 客户端将需要服务器的根证书;因为您打算使用自签名证书(一旦您修复服务器以完全使用密钥库),该证书就是它自己的根。在1.0.2(不是更早)中,您也可以使用非根信任锚,但默认情况下不会,但您还是没有。我假设OP_SERVER_CIPHER_PREFERENCE是为此目的,但你永远不会在实际文件上打开它或用它做任何其他事情,无论如何certbio库不能将BIO用于其信任库。你有三个选择:

  • 将证书放在文件或使用特殊哈希名称的目录中,并将文件和/或目录名称传递给SSL。如果您只想使用SSL_CTX_load_verify_locations选项的一个根(您自己的)更容易。

  • 将证书放入或添加到由OpenSSL库编译确定的默认文件或散列目录中,并调用CAfile;这通常类似于SSL_CTX_set_default_verify_paths/etc/pki。如果您想为多个程序或命令行/var/ssl使用相同的证书,则此共享位置通常更容易。

  • 使用BIO和/或其他方法(打开并)读取证书到内存中,构建自己的openssl包含它们,并将其放入你的X509_STORE。这比较复杂,所以除非您愿意,否则我不会扩展它。

(5)你的SSL_CTX是(至少在这种情况下?)地址字符串,而不是URL;虽然相关的东西是不同的,但认为它们是相同的会导致更多的问题。对于大多数程序,最好使用经典dest_url处理主机名称字符串并回退到gethostbyname,或者更好地使用&#34; new&#34; (自20世纪90年代以来)inet_addr可以处理名称和地址字符串以及IPv4和v6(自20世纪90年代以来也是新的但最终获得牵引力)。至少你应该检查getaddrinfo返回inet_addr,表明它失败了。