SSL을 사용한 통신에서 암호화 방식의 선택은 Client Hello와 Server Hello를 주고 받을 때 Cipher suites을 주고 받는다. 즉, 클라이언트에서는 자신이 지원하는 암호화 방식의 리스트(Cipher Suites)을 서버에 전송하고 서버에서는 클라이언트가 준 Cipher suites에서 자신이 지원하는 암호화 방식을 택하여 서버와 클라이언트간의 암호화 방식을 택하게 된다.
Client Hello
RandomCookie: GMT: 1226648498 bytes = { 200, 58, 111, 214, 101, 210, 35, 45, 30, 95, 248, 152, 164, 79, 190, 173, 246, 107, 75, 117, 87, 53, 57, 130, 181, 247, 4, 49 }
Session ID: {}
Cipher Suites: [SSL_RSA_WITH_RC4_128_MD5, SSL_RSA_WITH_RC4_128_SHA, TLS_RSA_WITH_AES_128_CBC_SHA, TLS_DHE_RSA_WITH_AES_128_CBC_SHA, TLS_DHE_DSS_WITH_AES_128_CBC_SHA, SSL_RSA_WITH_3DES_EDE_CBC_SHA, SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA, SSL_DHE_DSS_WITH_3DES_EDE_CBC_SHA, SSL_RSA_WITH_DES_CBC_SHA, SSL_DHE_RSA_WITH_DES_CBC_SHA, SSL_DHE_DSS_WITH_DES_CBC_SHA, SSL_RSA_EXPORT_WITH_RC4_40_MD5, SSL_RSA_EXPORT_WITH_DES40_CBC_SHA, SSL_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA, SSL_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA]
Compression Methods: { 0 }
***
위에 처럼 Cipher Suites 가 ClientHello에 실려 날아가게된다.
Server Hello
RandomCookie: GMT: 1226648580 bytes = { 5, 238, 245, 170, 224, 35, 204, 164, 117, 25, 117, 146, 156, 96, 177, 125, 190, 18, 220, 170, 62, 65, 6, 108, 174, 253, 27, 67 }
Session ID: {103, 15, 0, 0, 41, 170, 156, 123, 141, 32, 89, 224, 116, 103, 216, 190, 48, 141, 19, 213, 138, 211, 210, 218, 165, 23, 132, 90, 120, 77, 134, 26}
Cipher Suite: SSL_RSA_WITH_RC4_128_MD5
Compression Method: 0
***
위와 같이 서버에서는 Client Hello에 실린 Cipher Suites중 하나를 택하여 사용하게 된다. 이로써 이후의 통신은 선택된 방식을 사용하여 암호화 하게 된다.
사실 클라이언트 입장에서 자신이 지원하는 모든 Cipher suites를 전송하는게 맞으나.. 어느 회사에서 자신들은 3DES이상의 암호화만 안전한 것으로 판단하니 Cipher Suites를 바꿔 달라는 요청이 있었다.. ㅡㅡ;; 그럴바에 그냥 LDAP 서버에서 지원하는 암호화 리스트를 3DES 이상의 것들만 선택하는게 더 간단할텐데.. 왜 이런 삽질을 요구하는지 ㅡㅡ;;;;;
Java에서의 LDAP의 지원은 javax.naming 패키지에서 지원을 한다. LDAP 프로그래밍을 해 본 사람이면 아래의 구문은 익숙할 것이다.
LDAP 프로토콜을 사용한 통신
[code Java]
Hashtable<String,String> env = new Hashtable<String,String>();
env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldap://IP주소:389/");
env.put(Context.SECURITY_PRINCIPAL, "계정의 도메인");
env.put(Context.SECURITY_CREDENTIALS,"계정의 암호");
DirContext ctx = new InitialDirContext(env);
[/code]
위의 예제는 LDAP 프로토콜을 이용하여 389번 포트로 평문 통신을 하여 사용하는 것의 예제이다.
LDAPS 프로토콜을 사용한 통신
[code]
System.setProperty("javax.net.ssl.trustStore","c:\\java\\trust.keystore");
Hashtable<String,String> env = new Hashtable<String,String>();
env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldaps://IP주소:636/");
env.put(Context.SECURITY_PRINCIPAL, "계정의 도메인");
env.put(Context.SECURITY_CREDENTIALS,"계정의 암호");
env.put(Context.SECURITY_PROTOCOL,"ssl");
DirContext ctx = new InitialDirContext(env);
[/code]
위의 예제는 LDAPS 프로토콜을 사용하여 636 포트로 SSL암호화 통신을 한 예제이다. 다른게 있다면 포트가 636으로 바뀌었고 시스템 프로퍼티에 신뢰된 저장소(Trust store)를 지정한 것 그리고 암호화 프로토콜로 SSL을 지정 하였을 뿐이다.
이렇게 자바에서 LDAP 프로그래밍은 아주 간단히 큰 변화를 일으키지 않고도 많은 LDAP에서 LDAPS로의 변경이 가능하다. 여기서 문제가 되는 것은 이렇게 내부의 로직이 숨어 있어서 우리가 하고 싶은 Cipher Suites을 변경하는 작업이 어렵다는 점이다.
이런 문제점을 해결하기 위해 Jndi에서는 CustomSocketFactory라는 방식으로 지원을 한다. CustomSocketFactory는 이름에서도 알 수 있듯이 사용자 정의 소켓을 생성하도록 해주는 Factory 패턴이다. LDAP나 LDAPS 통신을 할때 기본적으로 SocketFactory나 이를 상속 받는 SSLSocketFactory를 사용하도록 되어 있다. 우리가 할일은 SSLSocketFactory를 상속받은 CustomSSLSocketFactory를 생성하고 jndi가 앞으로 소켓을 생성할때는 우리가 만든 CustomSSLSocketFactory를 사용하도록 지정하면 되는 것이다.
각설하고 소스코드는 다음과 같이 변한다.
[code]
System.setProperty("javax.net.ssl.trustStore","c:\\java\\trust.keystore");
Hashtable<String,String> env = new Hashtable<String,String>();
env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldaps://IP주소:636/");
env.put(Context.SECURITY_PRINCIPAL, "계정의 도메인");
env.put(Context.SECURITY_CREDENTIALS,"계정의 암호");
env.put(Context.SECURITY_PROTOCOL,"ssl");
env.put("java.naming.ldap.factory.socket", "CustomSSLSocketFactory");
DirContext ctx = new InitialDirContext(env);
[/code]
기본 LDAPS 소스코드에다 단지
env.put("java.naming.ldap.factory.socket", "CustomSSLSocketFactory"); 을 삽입한 것 뿐이다.
이는 LDAP에서 사용할 Socket은 CustomSSLSocketFactory 라는 클래스를 사용하여 생성하라는 것을 알려주는 것이다.
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import javax.net.SocketFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
public class CustomSSLSocketFactory extends SSLSocketFactory {
private SSLSocketFactory factory;
String suites[] = {
"SSL_RSA_WITH_3DES_EDE_CBC_SHA",
};
public CustomSSLSocketFactory() {
try {
SSLContext sslcontext = null;
if (sslcontext == null) {
sslcontext = SSLContext.getInstance("TLS");
sslcontext.init(null, // No KeyManager required
new TrustManager[] { new CustomTrustManager() },
new java.security.SecureRandom());
}
factory = (SSLSocketFactory) sslcontext.getSocketFactory();
} catch (Exception ex) {
ex.printStackTrace();
}
}
public static SocketFactory getDefault() {
return new CustomSSLSocketFactory();
}
public Socket createSocket(Socket socket, String s, int i, boolean flag) throws IOException {
SSLSocket sock = (SSLSocket)factory.createSocket(socket, s, i, flag);
sock.setEnabledCipherSuites(suites);
return sock;
}
public Socket createSocket(InetAddress inaddr, int i, InetAddress inaddr1, int j) throws IOException {
SSLSocket sock = (SSLSocket)factory.createSocket(inaddr, i, inaddr1, j);
sock.setEnabledCipherSuites(suites);
return sock;
}
public Socket createSocket(InetAddress inaddr, int i) throws IOException {
SSLSocket sock = (SSLSocket)factory.createSocket(inaddr, i);
sock.setEnabledCipherSuites(suites);
return sock;
}
public Socket createSocket(String s, int i, InetAddress inaddr, int j) throws IOException {
SSLSocket sock = (SSLSocket)factory.createSocket(s, i, inaddr, j);
sock.setEnabledCipherSuites(suites);
return sock;
}
public Socket createSocket(String s, int i) throws IOException {
SSLSocket sock = (SSLSocket)factory.createSocket(s, i);
sock.setEnabledCipherSuites(suites);
return sock;
}
public String[] getDefaultCipherSuites() {
return suites;
}
public String[] getSupportedCipherSuites() {
return suites;
}
}
[/code]
이름에서도 알 수 있듯이 Factory 패턴은 새로운 객체를 생성하는 디자인 패턴이다. 소스코드를 살펴보면 SSLSocket을 생성하고 Cipher suites를 지정한것 밖에 없다.
그리고 Trust Store를 찾기 위해 X509TrustManager의 구현을 참조하기 때문에 가짜로 CustomTrustManager라는 파일을 하나 생성하자. 그렇지 않으면 NULL Exception이 발생하니 가짜로라도 만들어 놓아야한다. 앞의 메인 프로그램에서 시스템 프로퍼티에 Trust Store를 지정하였으므로 가짜라도 상관이 없다.
CustomTurstManager.java
import java.security.cert.X509Certificate;
import javax.net.ssl.X509TrustManager;
public class CustomTrustManager implements X509TrustManager {
public void checkClientTrusted(X509Certificate[] cert, String authType) {
return;
}
public void checkServerTrusted(X509Certificate[] cert, String authType) {
return;
}
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}
[/code]
이로써 LDAPS에서 사용할 클라이언트 Cipher suites는 위의 CustomSSLSocketFactory에서 지정한 것으로 Client Hello를 날리게 될 것이다. 제대로 된 Cipher suites가 날라가는지 알고 싶으면 실행시에 -Djavax.net.debug=ssl 옵션을 줘서 실행하자. SSL 통신 절차들이 모두 디버그 코드로 화면에 출력 될 것이다.
참조 : JNDI Tutorial: SSL and Custom Sockets
Posted by 느긋나긋



