Java에서 HTTPS 좀 알고 구성해보자..
목차
개요
사용 인증서 항목
오류 만들기 및 오류 원인
인증서 검증 과정 삽질
결론:1
주의사항
TrustManager vs HostNameVerifier
결론:2
개요
사설인증서를 이용하여, HTTPS 통신을 실시한다. 그런데 당연히 잘 되지 않을 것이다. 구글링 결과 아래와 같이 HostnameVerifier를 진행하면 된다.
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
SSLSocketFactory socFac = SSLContextUtils.getSSLContext().getSocketFactory();
conn.setSSLSocketFactory( socFac );
conn.setHostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String s, SSLSession sslSession) {
return true;
}
});
return conn;
근데, 궁금한 것이 많다. TrustManager 인터페이스는 무엇이며, HostNameVerifier는 무엇일까? 그리고, 사설인증서는 어떻게 왜 제대로 통신이 되지 않는지 삽질을 해보자.
사용 인증서 항목
-
1개의 RootCA
-
1개의 intermediateCA
-
6개 인증서 = 2개 서버 * 3개의 Certificate( 와일드 카드 인증서, SAN 도메인 설정 인증서, SAN 미지정 인증서)
오류 만들기 및 오류 원인
1) java.security.cert.CertificateException: No subject alternative names present
원인: 인증서에 SAN 값에 IP 또는 도메인을 지정하지 않았음.
private static void matchIP(String var0, X509Certificate var1) throws CertificateException {
Collection var2 = var1.getSubjectAlternativeNames();
if (var2 == null) {
throw new CertificateException("No subject alternative names present");
2) javax.net.ssl.SSLHandshakeException: java.security.cert.CertificateException: No name matching ka.glaso.net found
원인: SAN에 도메인 값을 지정하였는데, 다른 도메인을 통해 접근함.
아래와 같이 SAN의 도메인 값을 ka.glaso.net으로 하였으나, k.glaso.net으로 접근하였음
private void matchDNS(String var1, X509Certificate var2) throws CertificateException {
try {
new SNIHostName(var1);
} catch (IllegalArgumentException var9) {
throw new CertificateException("Illegal given domain name: " + var1, var9);
}
Collection var3 = var2.getSubjectAlternativeNames();
if (var3 != null) {
boolean var4 = false;
Iterator var5 = var3.iterator();
while (var5.hasNext()) {
List var6 = (List) var5.next();
if ((Integer) var6.get(0) == 2) {
var4 = true;
String var7 = (String) var6.get(1);
if (this.isMatched(var1, var7)) {
return;
}
}
}
if (var4) {
throw new CertificateException("No subject alternative DNS name matching " + var1 + " found.");
}
}
Caused by: java.security.cert.CertificateException: No subject alternative DNS name matching k.glaso.net found.
at sun.security.util.HostnameChecker.matchDNS(HostnameChecker.java:204)
at sun.security.util.HostnameChecker.match(HostnameChecker.java:95)
at sun.security.ssl.X509TrustManagerImpl.checkIdentity(X509TrustManagerImpl.java:347)
at sun.security.ssl.X509TrustManagerImpl.checkTrusted(X509TrustManagerImpl.java:203)
at sun.security.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:126)
at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1428)
... 38 more
3) sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
원인: 오류의 말마따나, 접근하고자 하는 타깃의 인증서를 이용하여, 신뢰할 수 있는 유효한 인증서 체인을 찾을 수 없음.
내것은 신뢰관계가 설정되지 않은 사설인증서이다.
보통 Java에서 HTTPS 개발하는 사람들은 이 오류 메시지를 많이 확인할 수 있다.
인증서 검증 과정 삽질
제대로 분석을 하지 못했다. 왠지 모르겠지만, 디버깅이 너무 힘들어서 도중에 던졌다.
실질적으로 X509TrustManagerImpl클래스의 checkTrusted메소드가 서버 인증서의 검증을 실시하는 메소드이다. X509TrustManagerImpl은 JSSE 버전의 단순 유효성 검증 알고리즘과 PKIX 검증을 지원한다.
위와 같이 Stack Trace를 보면 ClientHandshaker.class 클래스에서 서버 인증서 검증을 시작한다. 위의 빨간 라인에서 checkServerTrusted 메소드를 호출하는데 해당 메소드는 X509TrustManager 인터페이스의 구현체이다. X509TrustManager 인터페이스는 TrustManager를 확장하였다. 즉, 우리가 정의하는 TrustManager이다(TrustManager를 사용자가 구현하여 TLS 핸드셰이킹 할 수 있음.). 그리고 최종 구현체는 X509TrustManagerImpl이다.
X509TrustManagerImpl 구현체의 checkServerTrusted메소드는 checkTrusted 메소드를 호출한다. 해당 메소드에서 본격적으로 서버 인증서의 검증을 실시한다.
checkIdentity를 통해 SAN의 IP 또는 DNS를 검증한다. checkIdentity 안의 실질적으로 위의 match 메소드가 IP/DNS를 체크하여 검증을 실시한다. 만약, 인증서에 SAN(Subject Alternative Name)이 존재하지 않으면 예외가 발생한다(HostNameVerifier true를 설정하면, checkIdentity 메소드를 건너뛴다.).
PKIX 검증을 하기 전, 이것저것 로직이 있는데, 잘 모르겠다.
오류가 나는 PKIX 검증 부분을 봐 보자.
sun.security.validator.PKIXValidator를 통해 PKIX(public key infrastructures based on X.509) 검증을 실시한다. PKIXValidator의 engineValidate메소드에서 이미 Trust Chain을 갖고 있다. 즉, 접근하고자 하는 엔티티의 종단 인증서, 그것을 발급한 중간 CA, 그것을 발급한 RootCA 총 3개의 인증서를 갖고 있다.
해당 인증서를 갖고 시스템이 갖고 있는 trustCerts를 통해 engineValidate를 실시한다. trustedCerts의 값들과 내가 갖고 있는데 인증서 체인을 DN 검증을 실시하는 것 같다(추측).
또한 위와 같이 trustedCerts는 92개의 인증서를 갖고 있는데 아마 이것들은 JDK의 cacerts 목록 같은데… 한번 확인해보자.
맞다. 아래와 같이 인증서 항목을 짤막하게 봤고, 총 개수도 92개이다. 아마 여기에 생성한 루트 인증서를 적재하면 될 것 같다.
여기가 끝은 아니지만 도저히 디버깅이 잘 안되는 관계로 패스한다.
결론:1
결론적으로 HTTPS 통신을 할 때 다음과 같은 경우의 수를 통해 진행할 수 있다.
1. 공인 인증서 사용 - HttpURLConnection 그냥 구성해도 괜찮음.
2. 사설 인증서 사용
2-1. cacerts에 인증서를 보관한다(trust certs에 추가하는 방안).
2-2. 2-1을 하지 않고 SSLContext 구성하여, HTTPS 통신을 한다.
2-2-1. SAN 값에 도메인 또는 IP가 지정되어 있음.
2-2-1-1. 단일 인증서 사용 - 단일 인증서만 키스토어 생성
2-2-1-2. 다중 인증서 사용 - 중간 CA 또는 Root CA 키스토어 생성
2-2-2. SAN 값이 존재하지 않음
2-2-2-1. HTTPS HostNameVerifier를 disable한다.
2-2-1-1-1. 단일 인증서 사용 - 단일 인증서만 키스토어 생성
2-2-1-2-2. 다중 인증서 사용 - 중간 CA 또는 Root CA 키스토어 생성
주의사항
사설 인증서를 허용하기 위해 TrustManager의 구현체에 대해 모든 검증에 대해 아무 로직 없이 return true를 절대 하지 말아야 한다.를 사용하는 이유가 사라진다.
예는 아래와 같은 코드이다.
https://stackoverflow.com/a/29075880/5213305
TrustManager vs HostNameVerifier
일단 TrustManager는 TLS/SSL 핸드쉐이킹 과정에 사용하는 인터페이스이며, HostNameVerifier는 HTTPS에서 사용하는 인터페이스이다(그래도 헷갈린다…).
https://stackoverflow.com/questions/23591513/hostnameverifier-vs-trustmanager
https://stackoverflow.com/a/6038836/5213305
결론:2
그래서 사설 인증서를 사용할 때 아래와 같이 SSLContext를 구성해보자.
public class SSLContextUtils {
private SSLContextUtils() {
throw new AssertionError();
}
public static SSLContext getSSLContext( byte[] cert, String TlsAlgoName, final boolean chkHostName ) throws KeyStoreException, NoSuchAlgorithmException, IOException, CertificateException, KeyManagementException {
CertificateFactory certFactory = CertificateFactory.getInstance( "X.509" );
X509Certificate kmsCertObj = (X509Certificate) certFactory.generateCertificate( new ByteArrayInputStream( cert ) );
String keyStoreType = KeyStore.getDefaultType();
KeyStore keyStore = KeyStore.getInstance( keyStoreType );
keyStore.load( null, null );
keyStore.setCertificateEntry( "kmsCert", kmsCertObj );
String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
TrustManagerFactory tmf = TrustManagerFactory.getInstance( tmfAlgorithm );
tmf.init( keyStore );
// tmf.init( (KeyStore) null ); // default cacerts in "JDK/jre/lib/security"
// 2019.06.11 - ehdvudee
// common java se 6 is not avaiable TLSv1.1 and TLSv1.2
// supported java se 6 is avaiable( TLSv1.1 : from u111, TLSv1.2 : from u121 )
SSLContext sc = SSLContext.getInstance( TlsAlgoName );
sc.init( null, tmf.getTrustManagers(), null );
return sc;
}
}
'개발관련 > (과거)메모' 카테고리의 다른 글
IntelliJ(JetBrain) 단축키 표(모음) 공유 (1) | 2020.05.18 |
---|---|
Tomcat 튜닝 가이드 (0) | 2020.05.14 |
Docker PostgreSQL 볼륨 매핑 권한 문제 without root (0) | 2020.03.18 |
JUnit과 함께하는 AssertJ (0) | 2019.11.12 |
Hashed Data RSA 서명할 때 주의사항 (0) | 2019.10.01 |