Programming/Java

JDK/JRE 6 + TLS 1.2 / javax.net.ssl.SSLException: Received fatal alert: protocol_version 조치

Lawmin 2024. 5. 3. 16:05

JDK/JRE 6 버전은 기본적으로 TLS 1.2 버전을 지원하지 않아 업그레이드가 필요하지만,

당장 작업이 여의치 않을 경우, JCE 및 BouncyCastle를 설치하여 사용 가능합니다.

 

1. 사이트에서 허용하는 Cipher와 내 서버에 설치된 JDK/JRE에서 지원하는 cipher 목록을 비교

맞는 cipher가 없는 경우 handshake 오류가 발생합니다.

 

1) 사이트 허용 TLS, Cipher 정보 확인

https://www.ssllabs.com/

위 사이트에서 URL 조회 후, Cipher Suites 항목의 # TLS 1.2 (suites in server-preferred order) 목록에 나옵니다.

 

2) 설치된 JDK/JRE 의 Supported Cipher 확인 (샘플 코드)

import java.util.Map;
import java.util.TreeMap;

import javax.net.ssl.SSLServerSocketFactory;

public class SecurityListings {
    public static void main(String[] args) {
        SSLServerSocketFactory ssf = (SSLServerSocketFactory)SSLServerSocketFactory.getDefault();

        TreeMap<String, Boolean> ciphers = new TreeMap();
        for (String cipher : ssf.getSupportedCipherSuites())
            ciphers.put(cipher, Boolean.FALSE);
        for (String cipher : ssf.getDefaultCipherSuites())
            ciphers.put(cipher, Boolean.TRUE);

        System.out.println("Default Cipher");
        for (Map.Entry<String, Boolean> cipher : ciphers.entrySet())
            System.out.printf("   %-5s%s%n", (cipher.getValue() ? '*' : ' '), cipher.getKey());
    }
}

 

2. JDK_HOME/jre/lib/security/java.security 백업 후, 수정

1) provider 추가 (맨 위에 놓지 않으면 CBC만 나오고 GCM 등의 cipher가 안 나올 수 있습니다.)

security.provider.1=org.bouncycastle.jce.provider.BouncyCastleProvider
security.provider.2=org.bouncycastle.jsse.provider.BouncyCastleJsseProvider

기존 provider는 +2 씩하여 변경 (기존 1은 3, 2는 4등 으로)

 

2) crypto.policy 확인 (JCE)

crypto.policy=limited 로 되어 있으면 crypto.policy=unlimited 로 변경

 

3. /jre/lib/ext 에 아래 BouncyCastle 파일 업로드

https://www.bouncycastle.org/download/bouncy-castle-java/#latest

bcprov-jdk15to18-x.y.z.jar

bctls-jdk15to18-x.y.z.jar

bcutil-jdk15to18-x.y.z.jar

 

4. /jre/lib/security에 JCE 파일 업로드 (필요 시)

https://www.oracle.com/java/technologies/jce-6-download.html

이미 있는 경우, 별도 업로드 하지 않고 2)번의 crypto.policy=unlimited 만 적용해도 무방합니다.

 

5. 테스트

1) BouncyCastle 정상 설치 여부 확인 (예시)

import java.security.Security;

public class BC {
	public static void main(String[] args) {
		if (Security.getProvider("BC") == null) {
			System.out.println(providerName + " NOT FOUND");
		} else {
			System.out.println(providerName + " FOUND");
		}
	}
}

 

2) https(TLS v1.2) 호출 가능 여부 확인 (예시)

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.URL;
import java.nio.charset.Charset;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;

public class HttpsTest {
	public static void main(String[] args) {
		int connectTimeout = 10000;
		int readTimeout = 10000;
	    String apiUrl = "https://APIURL";
    	try {
    		//System.setProperty("org.bouncycastle.jsse.client.assumeOriginalHostName", "true"); // certificate_unknown(46) 발생 시
    		SSLContext sc = SSLContext.getInstance("TLSv1.2");
    		sc.init(null, null, new java.security.SecureRandom());
    		URL urlObj = new URL(apiUrl);
    		HttpsURLConnection conn = (HttpsURLConnection) urlObj.openConnection();
    		HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
    		conn.setConnectTimeout(connectTimeout);    		
    		conn.setReadTimeout(readTimeout);
    		conn.setRequestMethod("POST");
    		conn.setRequestProperty("Content-Type","application/json");
    		conn.setRequestProperty("Accept-Charset","UTF-8");
			conn.setDoOutput(true);
			conn.setDoInput(true);

			String jsonParams = "{\"name\": \"value\"}";
			OutputStreamWriter wr = new OutputStreamWriter(conn.getOutputStream(),Charset.forName("UTF-8"));
			wr.write(jsonParams);
			wr.flush();

			BufferedReader br = new BufferedReader(new InputStreamReader((conn.getInputStream())));
			StringBuilder response = new StringBuilder();
			String output;
			while((output = br.readLine()) != null) {
				response.append(output);
			}
			System.out.println("response: " + response.toString());
    	} catch (Exception e) {
    		e.printStackTrace();
    	}
	}
}

※ System.setProperty("org.bouncycastle.jsse.client.assumeOriginalHostName", "true"); 이 부분은 테스트 시 아래와 같은 에러 메시지가 나올 때 넣어주면 됩니다. 

org.bouncycastle.jsse.provider.ProvTlsClient notifyConnectionClosed
org.bouncycastle.tls.TlsFatalAlert: certificate_unknown(46)
Caused by: java.security.cert.CertificateException: Unable to construct a valid chain
Caused by: java.security.cert.CertPathBuilderException: No issuer certificate for certificate in certification path found.