1. 개요
SSL(Secure Socket Layer)은 네트워크를 통한 통신에 Security을 제공하는 암호화 프로토콜입니다. 이 사용방법(예제)에서는 SSL 핸드셰이크 실패를 초래할 수 있는 다양한 시나리오와 그 방법에 대해 설명합니다.
JSSE를 사용하는 SSL 소개에서 SSL 의 기본 사항을 자세히 다룹니다.
2. 용어
Security 취약성으로 인해 표준 SSL이 TLS(전송 계층 Security)로 대체된다는 점에 유의해야 합니다. Java를 포함한 대부분의 프로그래밍 언어에는 SSL과 TLS를 모두 지원하는 라이브러리가 있습니다.
SSL이 시작된 이래로 OpenSSL 및 Java와 같은 많은 제품과 언어에는 TLS가 인수된 후에도 SSL에 대한 참조가 있었습니다. 이러한 이유로 이 사용방법(예제)의 나머지 부분에서는 일반적으로 암호화 프로토콜을 지칭하기 위해 SSL이라는 용어를 사용합니다.
3. 설정
이 사용방법(예제)의 목적을 위해 Java 소켓 API 를 사용하여 네트워크 연결을 시뮬레이트하는 간단한 서버 및 클라이언트 애플리케이션을 만듭니다.
3.1. 클라이언트 및 서버 생성
Java에서는 소켓을 사용 하여 네트워크를 통해 서버와 클라이언트 간의 통신 채널을 설정할 수 있습니다 . 소켓은 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. Java에서 인증서 생성
SSL은 네트워크 통신에서 비밀성, 무결성 및 신뢰성을 제공합니다. 인증서는 진정성을 확립하는 데 중요한 역할을 합니다.
일반적으로 이러한 인증서는 인증 기관에서 구입하고 서명하지만 이 사용방법(예제)에서는 자체 서명된 인증서를 사용합니다.
이를 위해 JDK와 함께 제공되는 keytool 을 사용할 수 있습니다.
$ keytool -genkey -keypass password \
-storepass password \
-keystore serverkeystore.jks
위의 명령은 대화형 셸을 시작하여 CN(일반 이름) 및 DN(고유 이름)과 같은 인증서에 대한 정보를 수집합니다. 모든 관련 세부 정보를 제공 하면 서버의 개인 키와 해당 공개 인증서가 포함된 serverkeystore.jks 파일이 생성됩니다.
serverkeystore.jks 는 Java 전용인 JKS(Java Key Store) 형식으로 저장됩니다. 요즈음 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 핸드셰이크의 일반적인 단계는 다음과 같습니다.
- 클라이언트는 사용할 수 있는 SSL 버전 및 암호 제품군 List을 제공합니다.
- 서버는 특정 SSL 버전 및 암호화 제품군에 동의하고 인증서로 응답합니다.
- 클라이언트는 인증서에서 공개 키를 추출하고 암호화된 "사전 마스터 키"로 응답합니다.
- 서버는 개인 키를 사용하여 "사전 마스터 키"를 해독합니다.
- 클라이언트와 서버는 교환된 "사전 마스터 키"를 사용하여 "공유 비밀"을 계산합니다.
- 클라이언트와 서버는 "공유 비밀"을 사용하여 성공적인 암호화 및 암호 해독을 확인하는 메시지를 교환합니다.
대부분의 단계는 모든 SSL 핸드셰이크에서 동일하지만 단방향 SSL과 양방향 SSL 사이에는 미묘한 차이가 있습니다. 이러한 차이점을 빠르게 검토해 보겠습니다.
4.1. 단방향 SSL의 핸드셰이크
위에서 언급한 단계를 참고하면 2단계는 인증서 교환을 언급합니다. 단방향 SSL에서는 클라이언트가 공용 인증서를 통해 서버를 신뢰할 수 있어야 합니다. 그러면 서버가 연결을 요청하는 모든 클라이언트를 신뢰하게 됩니다 . 서버가 Security 위험을 초래할 수 있는 클라이언트의 공용 인증서를 요청하고 유효성을 검사할 방법이 없습니다.
4.2. 양방향 SSL의 핸드셰이크
단방향 SSL을 사용하는 경우 서버는 모든 클라이언트를 신뢰해야 합니다. 그러나 양방향 SSL은 서버가 신뢰할 수 있는 클라이언트도 설정할 수 있는 기능을 추가합니다. 양방향 핸드셰이크 중에 클라이언트 와 서버는 성공적인 연결이 설정되기 전에 서로의 공용 인증서를 제시하고 수락해야 합니다 .
5. 핸드셰이크 실패 시나리오
이러한 빠른 검토를 완료하면 오류 시나리오를 보다 명확하게 볼 수 있습니다.
단방향 또는 양방향 통신에서 SSL 핸드셰이크는 여러 가지 이유로 실패할 수 있습니다. 이러한 각 이유를 살펴보고 실패를 시뮬레이션하고 이러한 시나리오를 피할 수 있는 방법을 이해할 것입니다.
이러한 각 시나리오에서 이전에 생성한 SimpleClient 및 SimpleServer 를 사용합니다.
5.1. 누락된 서버 인증서
SimpleServer 를 실행하고 SimpleClient 를 통해 연결해 봅시다 . "Hello World!" 메시지가 표시될 것으로 예상하지만 다음과 같은 예외가 표시됩니다.
Exception in thread "main" javax.net.ssl.SSLHandshakeException:
Received fatal alert: handshake_failure
이제 이것은 무언가 잘못되었음을 나타냅니다. 위 의 SSLHandshakeException 은 추상적인 방식으로 클라이언트가 서버에 연결할 때 인증서를 받지 못했다는 것을 나타냅니다.
이 문제를 해결하기 위해 이전에 생성한 키 저장소를 시스템 속성으로 서버에 전달하여 사용합니다.
-Djavax.net.ssl.keyStore=serverkeystore.jks -Djavax.net.ssl.keyStorePassword=password
키 저장소 파일 경로의 시스템 특성은 절대 경로이거나 키 저장소 파일이 서버를 시작하기 위해 Java 명령이 호출되는 동일한 디렉토리에 있어야 한다는 점에 유의하는 것이 중요합니다. 키 저장소에 대한 Java 시스템 속성은 상대 경로를 지원하지 않습니다.
이것이 우리가 기대하는 결과를 얻는 데 도움이 됩니까? 다음 하위 섹션에서 알아보겠습니다.
5.2. 신뢰할 수 없는 서버 인증서
이전 하위 섹션의 변경 사항으로 SimpleServer 및 SimpleClient 를 다시 실행하면 출력으로 무엇을 얻습니까?
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
음, 우리가 예상한 대로 정확히 작동하지 않았지만 다른 이유로 실패한 것 같습니다.
이 특별한 실패는 서버가 인증 기관(CA)에서 서명하지 않은 자체 서명 된 인증서를 사용하고 있기 때문에 발생합니다.
실제로 인증서가 기본 신뢰 저장소에 있는 것이 아닌 다른 것으로 서명될 때마다 이 오류가 표시됩니다. JDK의 기본 신뢰 저장소는 일반적으로 사용 중인 공통 CA에 대한 정보와 함께 제공됩니다.
여기서 이 문제를 해결하려면 SimpleClient 가 SimpleServer 에서 제공하는 인증서를 신뢰 하도록 강제해야 합니다 . 클라이언트에 시스템 속성으로 전달하여 이전에 생성한 신뢰 저장소를 사용하겠습니다.
-Djavax.net.ssl.trustStore=clienttruststore.jks -Djavax.net.ssl.trustStorePassword=password
이는 이상적인 솔루션이 아닙니다. 이상적인 시나리오에서는 자체 서명된 인증서가 아니라 클라이언트가 기본적으로 신뢰할 수 있는 인증 기관(CA)에서 인증한 인증서를 사용해야 합니다.
다음 하위 섹션으로 이동하여 지금 예상되는 출력을 얻는지 알아봅시다.
5.3. 클라이언트 인증서 누락
이전 하위 섹션의 변경 사항을 적용한 후 SimpleServer 및 SimpleClient를 한 번 더 실행해 보겠습니다.
Exception in thread "main" java.net.SocketException:
Software caused connection abort: recv failed
다시 말하지만 우리가 기대했던 것이 아닙니다. 여기서 SocketException 은 서버가 클라이언트를 신뢰할 수 없음을 알려줍니다. 양방향 SSL을 설정했기 때문입니다. SimpleServer 에는 다음이 있습니다.
((SSLServerSocket) listener).setNeedClientAuth(true);
위의 코드는 공개 인증서를 통한 클라이언트 인증에 SSLServerSocket 이 필요함을 나타냅니다.
이전 키 저장소와 신뢰 저장소를 만들 때 사용한 것과 유사한 방식으로 클라이언트용 키 저장소와 서버용 해당 신뢰 저장소를 만들 수 있습니다.
서버를 다시 시작하고 다음 시스템 속성을 전달합니다.
-Djavax.net.ssl.keyStore=serverkeystore.jks \
-Djavax.net.ssl.keyStorePassword=password \
-Djavax.net.ssl.trustStore=clienttruststore.jks \
-Djavax.net.ssl.trustStorePassword=password
그런 다음 다음 시스템 속성을 전달하여 클라이언트를 다시 시작합니다.
-Djavax.net.ssl.keyStore=serverkeystore.jks \
-Djavax.net.ssl.keyStorePassword=password \
-Djavax.net.ssl.trustStore=clienttruststore.jks \
-Djavax.net.ssl.trustStorePassword=password
마지막으로 원하는 출력을 얻었습니다.
Hello World!
5.4. 잘못된 인증서
위의 오류 외에도 인증서를 만든 방법과 관련된 다양한 이유로 인해 핸드셰이크가 실패할 수 있습니다. 일반적인 오류 중 하나는 잘못된 CN과 관련이 있습니다. 이전에 생성한 서버 키 저장소의 세부 정보를 살펴보겠습니다.
keytool -v -list -keystore serverkeystore.jks
위의 명령을 실행하면 키 저장소, 특히 소유자의 세부 정보를 볼 수 있습니다.
...
Owner: CN=localhost, OU=technology, O=baeldung, L=city, ST=state, C=xx
...
이 인증서 소유자의 CN은 localhost로 설정됩니다. 소유자의 CN은 서버의 호스트와 정확히 일치해야 합니다. 불일치가 있으면 SSLHandshakeException 이 발생 합니다.
localhost가 아닌 다른 것으로 CN을 사용하여 서버 인증서를 다시 생성해 봅시다. 이제 재생성된 인증서를 사용하여 SimpleServer 및 SimpleClient 를 실행 하면 즉시 실패합니다.
Exception in thread "main" javax.net.ssl.SSLHandshakeException:
java.security.cert.CertificateException:
No name matching localhost found
위의 예외 추적은 클라이언트가 찾을 수 없는 localhost라는 이름을 포함하는 인증서를 기대하고 있음을 분명히 나타냅니다.
JSSE는 기본적으로 호스트 이름 확인을 요구하지 않습니다 . HTTPS를 명시적으로 사용 하여 SimpleClient 에서 호스트 이름 확인을 활성화했습니다 .
SSLParameters sslParams = new SSLParameters();
sslParams.setEndpointIdentificationAlgorithm("HTTPS");
((SSLSocket) connection).setSSLParameters(sslParams);
호스트 이름 확인은 실패의 일반적인 원인이며 일반적으로 더 나은 Security을 위해 항상 시행되어야 합니다. 호스트 이름 확인 및 TLS Security에서의 중요성에 대한 자세한 내용은 이 문서 를 참조하십시오 .
5.5. 호환되지 않는 SSL 버전
현재 다양한 버전의 SSL 및 TLS를 포함한 다양한 암호화 프로토콜이 운영되고 있습니다.
앞서 언급한 바와 같이 일반적으로 SSL은 암호화 강도 때문에 TLS로 대체되었습니다. 암호화 프로토콜 및 버전은 클라이언트와 서버가 핸드셰이크 중에 동의해야 하는 추가 요소입니다.
예를 들어 서버가 SSL3의 암호화 프로토콜을 사용하고 클라이언트가 TLS1.3을 사용하는 경우 암호화 프로토콜에 동의할 수 없으며 SSLHandshakeException 이 생성됩니다.
SimpleClient 에서 프로토콜을 서버에 설정된 프로토콜과 호환되지 않는 것으로 변경해 보겠습니다.
((SSLSocket) connection).setEnabledProtocols(new String[] { "TLSv1.1" });
클라이언트를 다시 실행하면 SSLHandshakeException 이 발생합니다 .
Exception in thread "main" javax.net.ssl.SSLHandshakeException:
No appropriate protocol (protocol is disabled or cipher suites are inappropriate)
이러한 경우 예외 추적은 추상적이며 정확한 문제를 알려주지 않습니다. 이러한 유형의 문제를 해결하려면 클라이언트와 서버가 모두 동일하거나 호환되는 암호화 프로토콜을 사용하고 있는지 확인해야 합니다.
5.6. 호환되지 않는 암호 제품군
클라이언트와 서버는 또한 메시지를 암호화하는 데 사용할 암호화 제품군에 동의해야 합니다.
핸드셰이크 중에 클라이언트는 사용할 수 있는 암호 List을 제시하고 서버는 List에서 선택한 암호로 응답합니다. 서버는 적절한 암호를 선택할 수 없는 경우 SSLHandshakeException 을 생성합니다.
SimpleClient 에서 암호군을 서버에서 사용하는 암호군과 호환되지 않는 것으로 변경해 보겠습니다.
((SSLSocket) connection).setEnabledCipherSuites(
new String[] { "TLS_RSA_WITH_AES_128_GCM_SHA256" });
클라이언트를 다시 시작하면 SSLHandshakeException 이 발생합니다 .
Exception in thread "main" javax.net.ssl.SSLHandshakeException:
Received fatal alert: handshake_failure
다시 말하지만, 예외 추적은 매우 추상적이며 정확한 문제를 알려주지 않습니다. 이러한 오류에 대한 해결 방법은 클라이언트와 서버 모두에서 사용되는 활성화된 암호화 제품군을 확인하고 사용 가능한 공통 제품군이 하나 이상 있는지 확인하는 것입니다.
일반적으로 클라이언트와 서버는 다양한 암호화 제품군을 사용하도록 구성되므로 이 오류가 발생할 가능성이 적습니다. 이 오류가 발생하면 일반적으로 서버가 매우 선별적인 암호를 사용하도록 구성되었기 때문입니다. 서버는 Security상의 이유로 선택적 암호 집합을 적용하도록 선택할 수 있습니다.
6. 결론
이 사용방법(예제)에서는 Java 소켓을 사용하여 SSL을 설정하는 방법에 대해 배웠습니다. 그런 다음 단방향 및 양방향 SSL을 사용한 SSL 핸드셰이크에 대해 논의했습니다. 마지막으로 SSL 핸드셰이크가 실패할 수 있는 가능한 이유 List을 살펴보고 솔루션에 대해 논의했습니다.
항상 그렇듯이 예제 코드는 GitHub 에서 사용할 수 있습니다 .