Kegagalan Jabat Tangan SSL

Java Teratas

Saya baru sahaja mengumumkan kursus Learn Spring yang baru , yang berfokus pada asas-asas Spring 5 dan Spring Boot 2:

>> SEMAK KURSUS

1. Gambaran keseluruhan

Secured Socket Layer (SSL) adalah protokol kriptografi yang memberikan keselamatan dalam komunikasi melalui rangkaian. Dalam tutorial ini, kita akan membincangkan pelbagai senario yang boleh mengakibatkan kegagalan jabat tangan SSL dan bagaimana melakukannya.

Perhatikan bahawa Pengenalan SSL kami menggunakan JSSE merangkumi asas-asas SSL dengan lebih terperinci.

2. Terminologi

Penting untuk diperhatikan bahawa, kerana kerentanan keselamatan, SSL sebagai standard digantikan oleh Transport Layer Security (TLS). Sebilangan besar bahasa pengaturcaraan, termasuk Java, mempunyai perpustakaan untuk menyokong SSL dan TLS.

Sejak penubuhan SSL, banyak produk dan bahasa seperti OpenSSL dan Java telah merujuk kepada SSL yang mereka simpan walaupun setelah TLS mengambil alih. Atas sebab ini, dalam sisa tutorial ini, kami akan menggunakan istilah SSL untuk merujuk secara umum kepada protokol kriptografi.

3. Persediaan

Untuk tujuan tutorial ini, kami akan membuat aplikasi pelayan dan klien sederhana menggunakan Java Socket API untuk mensimulasikan sambungan rangkaian.

3.1. Membuat Pelanggan dan Pelayan

Di Jawa, kita boleh menggunakan s ockets untuk mewujudkan satu saluran komunikasi antara pelayan dan pelanggan melalui rangkaian . Soket adalah bahagian dari Java Secure Socket Extension (JSSE) di Java.

Mari mulakan dengan menentukan pelayan mudah:

int port = 8443; ServerSocketFactory factory = SSLServerSocketFactory.getDefault(); try (ServerSocket listener = factory.createServerSocket(port)) { SSLServerSocket sslListener = (SSLServerSocket) listener; sslListener.setNeedClientAuth(true); sslListener.setEnabledCipherSuites( new String[] { "TLS_DHE_DSS_WITH_AES_256_CBC_SHA256" }); sslListener.setEnabledProtocols( new String[] { "TLSv1.2" }); while (true) { try (Socket socket = sslListener.accept()) { PrintWriter out = new PrintWriter(socket.getOutputStream(), true); out.println("Hello World!"); } } }

Pelayan yang ditentukan di atas mengembalikan mesej "Hello World!" kepada pelanggan yang bersambung.

Seterusnya, mari tentukan pelanggan asas, yang akan kami sambungkan ke SimpleServer kami :

String host = "localhost"; int port = 8443; SocketFactory factory = SSLSocketFactory.getDefault(); try (Socket connection = factory.createSocket(host, port)) { ((SSLSocket) connection).setEnabledCipherSuites( new String[] { "TLS_DHE_DSS_WITH_AES_256_CBC_SHA256" }); ((SSLSocket) connection).setEnabledProtocols( new String[] { "TLSv1.2" }); SSLParameters sslParams = new SSLParameters(); sslParams.setEndpointIdentificationAlgorithm("HTTPS"); ((SSLSocket) connection).setSSLParameters(sslParams); BufferedReader input = new BufferedReader( new InputStreamReader(connection.getInputStream())); return input.readLine(); }

Pelanggan kami mencetak mesej yang dikembalikan oleh pelayan.

3.2. Membuat Sijil di Java

SSL memberikan kerahsiaan, integriti, dan keaslian dalam komunikasi rangkaian. Sijil memainkan peranan penting sejauh mana membuktikan keaslian.

Biasanya, sijil ini dibeli dan ditandatangani oleh Lembaga Perakuan, tetapi untuk tutorial ini, kami akan menggunakan sijil yang ditandatangani sendiri.

Untuk mencapainya, kita boleh menggunakan keytool, yang disertakan bersama JDK:

$ keytool -genkey -keypass password \ -storepass password \ -keystore serverkeystore.jks

Perintah di atas memulakan shell interaktif untuk mengumpulkan maklumat untuk sijil seperti Common Name (CN) dan Distinguished Name (DN). Apabila kami memberikan semua perincian yang relevan, ia akan menghasilkan filekeykeystore.jks , yang mengandungi kunci peribadi pelayan dan sijil awamnya.

Perhatikan bahawa serverkeystore.jks disimpan dalam format Java Key Store (JKS), yang dimiliki oleh Java. Hari-hari ini, keytool akan mengingatkan kita bahawa kita harus mempertimbangkan untuk menggunakan PKCS # 12, yang juga disokongnya.

Kita boleh menggunakan keytool untuk mengeluarkan sijil awam dari fail keystore yang dihasilkan:

$ keytool -export -storepass password \ -file server.cer \ -keystore serverkeystore.jks

Perintah di atas mengeksport sijil awam dari kedai kunci sebagai pelayan fail.cer . Mari gunakan sijil yang dieksport untuk pelanggan dengan menambahkannya ke kedai amanahnya:

$ keytool -import -v -trustcacerts \ -file server.cer \ -keypass password \ -storepass password \ -keystore clienttruststore.jks

Kami kini telah menghasilkan kedai kunci untuk pelayan dan kedai amanah yang sesuai untuk pelanggan. Kami akan membahas penggunaan fail yang dihasilkan ini ketika kami membincangkan kemungkinan kegagalan berjabat tangan.

Dan perincian lebih lanjut mengenai penggunaan kedai kunci Java boleh didapati di tutorial sebelumnya.

4. Jabat Tangan SSL

Jabat tangan SSL adalah mekanisme di mana pelanggan dan pelayan menetapkan kepercayaan dan logistik yang diperlukan untuk menjamin sambungan mereka melalui rangkaian .

Ini adalah prosedur yang sangat disusun dan memahami perinciannya dapat membantu memahami mengapa ia sering gagal, yang ingin kita bahas di bahagian seterusnya.

Langkah-langkah khas dalam jabat tangan SSL adalah:

  1. Pelanggan menyediakan senarai kemungkinan versi SSL dan cipher suite untuk digunakan
  2. Pelayan bersetuju pada versi SSL dan suite cipher tertentu, membalas dengan sijilnya
  3. Pelanggan mengekstrak kunci awam dari sijil bertindak balas dengan "kunci pra-master" yang disulitkan
  4. Pelayan menyahsulitkan "kunci pra-master" menggunakan kunci peribadinya
  5. Pelanggan dan pelayan mengira "rahsia bersama" menggunakan "kunci pra-master" yang ditukar
  6. Mesej pertukaran pelanggan dan pelayan yang mengesahkan penyulitan dan penyahsulitan yang berjaya menggunakan "rahsia bersama"

Walaupun kebanyakan langkahnya sama untuk jabat tangan SSL, terdapat perbezaan yang halus antara SSL satu arah dan dua arah. Mari kita kaji perbezaan ini dengan cepat.

4.1. Jabat Tangan dalam SSL Sehala

Sekiranya kita merujuk kepada langkah-langkah yang disebutkan di atas, langkah kedua menyebut pertukaran sijil. SSL sehala memerlukan pelanggan mempercayai pelayan melalui sijil awamnya. Ini membiarkan pelayan mempercayai semua pelanggan yang meminta sambungan. Tidak ada cara bagi pelayan untuk meminta dan mengesahkan sijil awam dari klien yang boleh menimbulkan risiko keselamatan.

4.2. Jabat Tangan dalam SSL Dua Hala

Dengan SSL sehala, pelayan mesti mempercayai semua pelanggan. Tetapi, SSL dua hala menambah kemampuan pelayan untuk dapat mewujudkan klien yang dipercayai. Semasa berjabat tangan dua hala, kedua-dua pelanggan dan pelayan mesti menunjukkan dan menerima sijil awam masing-masing sebelum hubungan yang berjaya dapat dijalin.

5. Senario Kegagalan Tangan

Setelah melakukan tinjauan pantas itu, kita dapat melihat senario kegagalan dengan lebih jelas.

Jabat tangan SSL, dalam komunikasi satu arah atau dua arah, boleh gagal kerana pelbagai sebab. Kami akan meneliti setiap sebab ini, meniru kegagalan dan memahami bagaimana kita dapat mengelakkan senario seperti ini.

Dalam setiap senario ini, kami akan menggunakan SimpleClient dan SimpleServer yang kami buat sebelumnya.

5.1. Sijil Pelayan Hilang

Mari cuba jalankan SimpleServer dan sambungkannya melalui SimpleClient . Walaupun kami berharap dapat melihat mesej "Hello World!", Kami diberikan pengecualian:

Exception in thread "main" javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure

Now, this indicates something went wrong. The SSLHandshakeException above, in an abstract manner, is stating that the client when connecting to the server did not receive any certificate.

To address this issue, we will use the keystore we generated earlier by passing them as system properties to the server:

-Djavax.net.ssl.keyStore=clientkeystore.jks -Djavax.net.ssl.keyStorePassword=password

It's important to note that the system property for the keystore file path should either be an absolute path or the keystore file should be placed in the same directory from where the Java command is invoked to start the server. Java system property for keystore does not support relative paths.

Does this help us get the output we are expecting? Let's find out in the next sub-section.

5.2. Untrusted Server Certificate

As we run the SimpleServer and the SimpleClient again with the changes in the previous sub-section, what do we get as output:

Exception in thread "main" javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

Well, it did not work exactly as we expected, but looks like it has failed for a different reason.

This particular failure is caused by the fact that our server is using a self-signed certificate which is not signed by a Certificate Authority (CA).

Really, any time the certificate is signed by something other than what is in the default truststore, we'll see this error. The default truststore in JDK typically ships with information about common CAs in use.

To solve this issue here, we will have to force SimpleClient to trust the certificate presented by SimpleServer. Let's use the truststore we generated earlier by passing them as system properties to the client:

-Djavax.net.ssl.trustStore=clienttruststore.jks -Djavax.net.ssl.trustStorePassword=password

Please note that this is not an ideal solution. In an ideal scenario, we should not use a self-signed certificate but a certificate which has been certified by a Certificate Authority (CA) which clients can trust by default.

Let's go to the next sub-section to find out if we get our expected output now.

5.3. Missing Client Certificate

Let's try one more time running the SimpleServer and the SimpleClient, having applied the changes from previous sub-sections:

Exception in thread "main" java.net.SocketException: Software caused connection abort: recv failed

Again, not something we expected. The SocketException here tells us that the server could not trust the client. This is because we have set up a two-way SSL. In our SimpleServer we have:

((SSLServerSocket) listener).setNeedClientAuth(true);

The above code indicates an SSLServerSocket is required for client authentication through their public certificate.

We can create a keystore for the client and a corresponding truststore for the server in a way similar to the one that we used when creating the previous keystore and truststore.

We will restart the server and pass it the following system properties:

-Djavax.net.ssl.keyStore=serverkeystore.jks \ -Djavax.net.ssl.keyStorePassword=password \ -Djavax.net.ssl.trustStore=servertruststore.jks \ -Djavax.net.ssl.trustStorePassword=password

Then, we will restart the client by passing these system properties:

-Djavax.net.ssl.keyStore=clientkeystore.jks \ -Djavax.net.ssl.keyStorePassword=password \ -Djavax.net.ssl.trustStore=clienttruststore.jks \ -Djavax.net.ssl.trustStorePassword=password

Finally, we have the output we desired:

Hello World!

5.4. Incorrect Certificates

Apart from the above errors, a handshake can fail due to a variety of reasons related to how we have created the certificates. One common error is related to an incorrect CN. Let's explore the details of the server keystore we created previously:

keytool -v -list -keystore serverkeystore.jks

When we run the above command, we can see the details of the keystore, specifically the owner:

... Owner: CN=localhost, OU=technology, O=baeldung, L=city, ST=state, C=xx ...

The CN of the owner of this certificate is set to localhost. The CN of the owner must exactly match the host of the server. If there is any mismatch it will result in an SSLHandshakeException.

Let's try to regenerate the server certificate with CN as anything other than localhost. When we use the regenerated certificate now to run the SimpleServer and SimpleClient it promptly fails:

Exception in thread "main" javax.net.ssl.SSLHandshakeException: java.security.cert.CertificateException: No name matching localhost found

The exception trace above clearly indicates that the client was expecting a certificate bearing the name as localhost which it did not find.

Please note that JSSE does not mandate hostname verification by default. We have enabled hostname verification in the SimpleClient through explicit use of HTTPS:

SSLParameters sslParams = new SSLParameters(); sslParams.setEndpointIdentificationAlgorithm("HTTPS"); ((SSLSocket) connection).setSSLParameters(sslParams);

Hostname verification is a common cause of failure and in general and should always be enforced for better security. For details on hostname verification and its importance in security with TLS, please refer to this article.

5.5. Incompatible SSL Version

Currently, there are various cryptographic protocols including different versions of SSL and TLS in operation.

As mentioned earlier, SSL, in general, has been superseded by TLS for its cryptographic strength. The cryptographic protocol and version are an additional element that a client and a server must agree on during a handshake.

For example, if the server uses a cryptographic protocol of SSL3 and the client uses TLS1.3 they cannot agree on a cryptographic protocol and an SSLHandshakeException will be generated.

In our SimpleClient let's change the protocol to something that is not compatible with the protocol set for the server:

((SSLSocket) connection).setEnabledProtocols(new String[] { "TLSv1.1" });

When we run our client again, we will get an SSLHandshakeException:

Exception in thread "main" javax.net.ssl.SSLHandshakeException: No appropriate protocol (protocol is disabled or cipher suites are inappropriate)

The exception trace in such cases is abstract and does not tell us the exact problem. To resolve these types of problems it is necessary to verify that both the client and server are using either the same or compatible cryptographic protocols.

5.6. Incompatible Cipher Suite

The client and server must also agree on the cipher suite they will use to encrypt messages.

During a handshake, the client will present a list of possible ciphers to use and the server will respond with a selected cipher from the list. The server will generate an SSLHandshakeException if it cannot select a suitable cipher.

In our SimpleClient let's change the cipher suite to something that is not compatible with the cipher suite used by our server:

((SSLSocket) connection).setEnabledCipherSuites( new String[] { "TLS_RSA_WITH_AES_128_GCM_SHA256" });

When we restart our client we will get an SSLHandshakeException:

Exception in thread "main" javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure

Again, the exception trace is quite abstract and does not tell us the exact problem. The resolution to such an error is to verify the enabled cipher suites used by both the client and server and ensure that there is at least one common suite available.

Normally, clients and servers are configured to use a wide variety of cipher suites so this error is less likely to happen. If we encounter this error it is typically because the server has been configured to use a very selective cipher. A server may choose to enforce a selective set of ciphers for security reasons.

6. Conclusion

Dalam tutorial ini, kami belajar tentang mengatur SSL menggunakan soket Java. Kemudian kami membincangkan jabat tangan SSL dengan SSL sehala dan dua hala. Akhirnya, kami meneliti senarai kemungkinan sebab bahawa jabat tangan SSL mungkin gagal dan membincangkan penyelesaiannya.

Seperti biasa, kod untuk contoh boleh didapati di GitHub.

Bahagian bawah Java

Saya baru sahaja mengumumkan kursus Learn Spring yang baru , yang berfokus pada asas-asas Spring 5 dan Spring Boot 2:

>> SEMAK KURSUS