SSL 핸드 셰이크 실패

자바 탑

방금 Spring 5 및 Spring Boot 2의 기본 사항에 초점을 맞춘 새로운 Learn Spring 과정을 발표했습니다 .

>> 과정 확인

1. 개요

SSL (Secure Socket Layer)은 네트워크를 통한 통신에서 보안을 제공하는 암호화 프로토콜입니다. 이 자습서에서는 SSL 핸드 셰이크 실패를 초래할 수있는 다양한 시나리오와 그 방법에 대해 설명합니다.

JSSE를 사용하는 SSL 소개에서는 SSL의 기본 사항을 자세히 다룹니다.

2. 용어

보안 취약점으로 인해 SSL을 표준으로 사용하는 것이 TLS (Transport Layer Security)로 대체된다는 점에 유의해야합니다. Java를 포함한 대부분의 프로그래밍 언어에는 SSL과 TLS를 모두 지원하는 라이브러리가 있습니다.

SSL이 시작된 이래로 OpenSSL 및 Java와 같은 많은 제품 및 언어에는 TLS가 인수 된 후에도 유지하는 SSL에 대한 참조가있었습니다. 따라서이 자습서의 나머지 부분에서는 일반적으로 암호화 프로토콜을 지칭하기 위해 SSL이라는 용어를 사용합니다.

3. 설정

이 튜토리얼에서는 네트워크 연결을 시뮬레이션하기 위해 Java Socket API를 사용하여 간단한 서버 및 클라이언트 응용 프로그램을 만듭니다.

3.1. 클라이언트 및 서버 생성

자바에서 우리가 사용할 수있는 S의 네트워크를 통해 서버와 클라이언트 간의 통신 채널을 설정하는 ockets을 . 소켓은 Java의 JSSE (Java Secure Socket Extension)의 일부입니다.

간단한 서버를 정의하는 것으로 시작하겠습니다.

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!"); } } }

위에 정의 된 서버는 "Hello World!"라는 메시지를 반환합니다. 연결된 클라이언트에.

다음으로 SimpleServer에 연결할 기본 클라이언트를 정의하겠습니다 .

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(); }

클라이언트는 서버가 반환 한 메시지를 인쇄합니다.

3.2. 자바로 인증서 생성

SSL은 네트워크 통신에서 기밀성, 무결성 및 신뢰성을 제공합니다. 인증서는 진위성을 확립하는 데 중요한 역할을합니다.

일반적으로 이러한 인증서는 인증 기관에서 구매하고 서명하지만이 자습서에서는 자체 서명 된 인증서를 사용합니다.

이를 위해 JDK와 함께 제공되는 keytool 을 사용할 수 있습니다 .

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

위의 명령은 대화 형 셸을 시작하여 CN (일반 이름) 및 DN (고유 이름)과 같은 인증서에 대한 정보를 수집합니다. 모든 관련 세부 정보를 제공 하면 서버의 개인 키와 공개 인증서가 포함 된 serverkeystore.jks 파일이 생성됩니다 .

참고 serverkeystore.jks는 자바 독점 자바 키 저장소 (JKS) 형식으로 저장됩니다. 요즘 keytool 은 또한 지원하는 PKCS # 12 사용을 고려해야 함을 알려줍니다.

추가로 keytool 을 사용 하여 생성 된 키 저장소 파일에서 공용 인증서를 추출 할 수 있습니다 .

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

위의 명령은 키 저장소에서 server.cer 파일로 공개 인증서를 내 보냅니다 . 신뢰 저장소에 추가하여 클라이언트에 대해 내 보낸 인증서를 사용하겠습니다.

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

이제 서버용 키 저장소와 클라이언트 용 해당 신뢰 저장소를 생성했습니다. 가능한 핸드 셰이크 실패에 대해 논의 할 때 이러한 생성 된 파일의 사용에 대해 살펴 보겠습니다.

Java의 키 저장소 사용에 대한 자세한 내용은 이전 자습서에서 찾을 수 있습니다.

4. SSL 핸드 셰이크

SSL 핸드 셰이크는 클라이언트와 서버가 네트워크를 통한 연결을 보호하는 데 필요한 신뢰와 물류를 설정하는 메커니즘 입니다.

이것은 매우 잘 짜여진 절차이며 세부 사항을 이해하면 종종 실패하는 이유를 이해하는 데 도움이 될 수 있습니다. 다음 섹션에서 다룰 예정입니다.

SSL 핸드 셰이크의 일반적인 단계는 다음과 같습니다.

  1. 클라이언트는 사용할 수있는 SSL 버전 및 암호 제품군 목록을 제공합니다.
  2. 서버는 특정 SSL 버전 및 암호 제품군에 동의하여 인증서로 다시 응답합니다.
  3. 클라이언트는 인증서에서 공개 키를 추출하여 암호화 된 "사전 마스터 키"로 응답합니다.
  4. 서버는 개인 키를 사용하여 "사전 마스터 키"를 해독합니다.
  5. 클라이언트와 서버는 교환 된 "사전 마스터 키"를 사용하여 "공유 비밀"을 계산합니다.
  6. 클라이언트와 서버는 "공유 비밀"을 사용하여 성공적인 암호화 및 복호화를 확인하는 메시지를 교환합니다.

대부분의 단계는 모든 SSL 핸드 셰이크에서 동일하지만 단방향 SSL과 양방향 SSL 사이에는 미묘한 차이가 있습니다. 이러한 차이점을 빠르게 검토해 보겠습니다.

4.1. 단방향 SSL의 핸드 셰이크

위에서 언급 한 단계를 참조하면 2 단계에서는 인증서 교환을 언급합니다. 단방향 SSL을 사용하려면 클라이언트가 공용 인증서를 통해 서버를 신뢰할 수 있어야합니다. 이렇게 하면 서버가 연결을 요청하는 모든 클라이언트를 신뢰하게됩니다 . 보안 위험을 초래할 수있는 클라이언트의 공용 인증서를 서버가 요청하고 유효성을 검사하는 방법은 없습니다.

4.2. 양방향 SSL의 핸드 셰이크

단방향 SSL을 사용하는 경우 서버는 모든 클라이언트를 신뢰해야합니다. 그러나 양방향 SSL은 서버가 신뢰할 수있는 클라이언트를 설정할 수있는 기능을 추가합니다. 양방향 핸드 셰이크 동안 클라이언트와 서버는 성공적인 연결을 설정하기 전에 서로의 공용 인증서제시하고 수락해야합니다 .

5. 핸드 셰이크 실패 시나리오

빠른 검토를 마치면 더 명확하게 실패 시나리오를 볼 수 있습니다.

단방향 또는 양방향 통신에서 SSL 핸드 셰이크는 여러 가지 이유로 실패 할 수 있습니다. 이러한 각 이유를 살펴보고 실패를 시뮬레이션하고 그러한 시나리오를 피할 수있는 방법을 이해할 것입니다.

이러한 각 시나리오에서 앞서 만든 SimpleClientSimpleServer를 사용합니다 .

5.1. 누락 된 서버 인증서

SimpleServer 를 실행 하고 SimpleClient를 통해 연결해 보겠습니다 . "Hello World!"메시지가 표시 될 것으로 예상되지만 예외가 표시됩니다.

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

이 튜토리얼에서는 Java 소켓을 사용하여 SSL을 설정하는 방법을 배웠습니다. 그런 다음 단방향 및 양방향 SSL을 사용한 SSL 핸드 셰이크에 대해 논의했습니다. 마지막으로 SSL 핸드 셰이크가 실패 할 수있는 가능한 원인 목록을 살펴보고 솔루션에 대해 논의했습니다.

항상 그렇듯이 예제 코드는 GitHub에서 사용할 수 있습니다.

자바 바닥

방금 Spring 5 및 Spring Boot 2의 기본 사항에 초점을 맞춘 새로운 Learn Spring 과정을 발표했습니다 .

>> 과정 확인