SSL 서버
지난 테크팁 'JSSE를 이용한 안전한 커뮤니케이션'에서 클라이언트측으로부터의 보안 HTTP 요청과 반응(HTTPS또는 HTTP over SSL)을 어떻게 핸들링하는지 배웠다. 이번 팁에서는 서버측 부분의 SSL 커뮤니케이션에 대해서 알아보기로 한다.
첫번째 팁 Working with Selectors에서와 같이 이번 팁의 서버는 에코 서버이다. 즉 서버는 클라이언트로부터 수신한 것을 단순히 클라이언트에게 재송신한다.
Again, 클라이언트 측 테크팁에서의 경우과 같이 서버를 생성하기 위해 먼저 소켓 팩토리를 얻는다. SSL 서버용 소켓을 위해서 SSLServerSocketFactory 타입 팩토리를 이용한다. SSLServerSocketFactory는 javax.net.ssl 패키지에 있다.
ServerSocketFactory sslserversocketfactory =
SSLServerSocketFactory.getDefault();
그 후 서버 소켓을 얻고 accept와 연결되기를 기다린다.
ServerSocket serverSocket =
serverSocketFactory.createServerSocket(PORT_NUM);
Socket socket = serverSocket.accept();
나머지 서버 코드는 단순하다. "SelectorTest"팁과 비슷하여, 클라이언트가 전송하는 것을 단지 읽고 다시 재전송하기만 하면 된다. 다음은 서버 코드 전체이다.
import javax.net.ssl.*;
import javax.net.*;
import java.io.*;
import java.net.*;
public class EchoServer {
private static final int PORT_NUM = 6789;
public static void main(String args[]) {
ServerSocketFactory serverSocketFactory =
SSLServerSocketFactory.getDefault();
ServerSocket serverSocket = null;
try {
serverSocket =
serverSocketFactory.createServerSocket(PORT_NUM);
} catch (IOException ignored) {
System.err.println("Unable to create server");
System.exit(-1);
}
while(true) {
Socket socket = null;
try {
socket = serverSocket.accept();
InputStream is = socket.getInputStream();
BufferedReader br = new BufferedReader(
new InputStreamReader(is, "US-ASCII"));
OutputStream os = socket.getOutputStream();
Writer writer =
new OutputStreamWriter(os, "US-ASCII");
PrintWriter out = new PrintWriter(writer, true);
String line = null;
while ((line = br.readLine()) != null) {
out.println(line);
}
} catch (IOException exception) {
exception.printStackTrace();
} finally {
if (socket != null) {
try {
socket.close();
} catch (IOException ignored) {
}
}
}
}
}
}
그러나 이 프로그램을 구동하기 전에 한가지 문제가 있다. 인증서가 없다는 것이다. 특정 인증서 없이 이 프로그램을 구동하면 SSLException을 얻게 된다.
javax.net.ssl.SSLException: No available certificate
corresponds to the SSL cipher suites which are enabled.
at com.sun.net.ssl.internal.ssl.SSLServerSocketImpl.a(DashoA6275)
at com.sun.net.ssl.internal.ssl.SSLServerSocketImpl.accept(DashoA6275)
at EchoServer.main(EchoServer.java:21)
SDK와 함께 제공되는 키툴(keytool) 프로그램으로 인증서를 생성할 수 있다. 키페어(keypair)를 생성하는 -genkey 옵션, 키 스토어 파일(key store file)을 지정하는 -keystore옵션, 암호 알고리즘을 지정하는 -keyalg 옵션이 포함된 keytool 커멘드를 실행하자.
keytool -genkey -keystore testStore -keyalg RSA
그러면 몇가지 정보입력을 위한 질문을 받게될 것이다. 적절한 정보를 제공하면 된다. 다음은 예상되는 다이얼로그의 한 예이다.
Enter keystore password: tutorial
What is your first and last name?
[Unknown]: Sun Tutorial
What is the name of your organizational unit?
[Unknown]: Sun
What is the name of your organization?
[Unknown]: Sun
What is the name of your City or Locality?
[Unknown]: Santa Clara
What is the name of your State or Province?
[Unknown]: CA
What is the two-letter country code for this unit?
[Unknown]: US
Is CN=Sun Tutorial, OU=Sun, O=Sun, L=Santa Clara,
ST=CA, C=US correct?
[no]: yes
Enter key password for <mykey>
(RETURN if same as keystore password):
일반적으로 CN 엔트리(이름)는 필수적인 요청사항은 아니지만, 서버의 호스트명이 되어야한다. 위의 예에서 클라이언트가 localhost로 서버에 접근하기 때문에, 그 이름이 필히 사용될 것이다.
커멘드를 구동한 후에는, 작업하고 있는 디렉토리에서 testStore라는 이름의 새로운 파일을 발견하게 될 것이다. 이제 SSL을 이용하여 서버를 구동시킬 수 있다. 서버를 구동하는 커멘드를 송신할 때 javax.net.ssl.keyStore 속성으로 키 스토어를 식별하고 javax.net.ssl.keyStorePassword로 키 스토어의 패스워드를 식별한다.
java -Djavax.net.ssl.keyStore=testStore
-Djavax.net.ssl.keyStorePassword=tutorial EchoServer
물론 이 때 서버와 커뮤니케이션 할 클라이언트가 필요하다. 다음의 클라이언트는 커멘드 라인으로부터의 입력을 읽고, 서버에 그 입력을 송신한 후 되돌아 온 응답을 작성한다.
import javax.net.ssl.*;
import javax.net.*;
import java.io.*;
import java.net.*;
public class EchoClient {
private static int PORT_NUM = 6789;
private static String host = "localhost";
public static void main(String args[])
throws IOException {
SocketFactory socketFactory =
SSLSocketFactory.getDefault();
Socket socket = socketFactory.createSocket(
host, PORT_NUM);
BufferedReader br = new BufferedReader(
new InputStreamReader(System.in, "US-ASCII"));
PrintWriter out = new PrintWriter(
new OutputStreamWriter(
socket.getOutputStream(), "US-ASCII"), true);
BufferedReader socketBr = new BufferedReader(
new InputStreamReader(
socket.getInputStream(), "US-ASCII"));
String string = null;
System.out.print("First line: ");
while (!(string = br.readLine()).equals("")) {
out.println(string);
String line = socketBr.readLine();
System.out.println("Got Back: " + line);
System.out.print("Next line: ");
}
socket.close();
}
}
클라이언트를 컴파일하자. 클라이언트를 구동하기 위해서는 서버를 시작할 때 지정한 것과 같은 인증서를 지정해야한다. 그렇지 않으면 클라이언트가 서버에 연결하기 위해 시도할 때 서버에 SSLHandshakeException이 발생한다.
javax.net.ssl.SSLHandshakeException: Remote host closed
connection during handshake
클라이언트를 시작할 때 키 스토어 대신 트러스트 스토어로서 인증서를 참조해보자.
java -Djavax.net.ssl.trustStore=testStore
-Djavax.net.ssl.trustStorePassword=tutorial EchoClient
그러면 클라이언트가 SSL을 통해 서버와 커뮤니케이션 하게 될 것이다.
First line: Hello
Got Back: Hello
Next line: There
Got Back: There
Next line: Server
Got Back: Server
한 머신 안에서 인증서를 공유하는 것은 쉽다. 그러나 여러 머신에서 인증서를 공유하기 위해서는 외부 클라이언트 머신에 인증서를 발행하는 것이 필요하다. 단순히 "testStore" 키스토어를 복사할 수 있지만, 그렇게 되면 사용자의 개인 키를 노출하게 되어 아무나 그 사용자인양 위장할 수 있게 된다. 따라서 그 대신에 키툴 프로그램의 -export 와 -import옵션을 사용할 수 있다. 키스토어의 공개 가능한 인증서를 익스포트하자. 그러면 당신을 신뢰하는 누군가가 그들의 머신으로 인증서를 임포트할 것이다. 이 작업은 여러 머신에 여러 번 가능하다. 외부 머신이 인증서를 임포트한 후에는 그 머신의 클라이언트와 SSL을 통해 커뮤니케이션 할 수 있다.
SSL 사용에 대한 좀 더 자세한 정보를 원한다면 Java Secure Socket Extension (JSSE) Reference Guide를 참고하기 바란다.
'Java' 카테고리의 다른 글
자바 중복 제거 (0) | 2010.03.04 |
---|---|
[자바] 현재 시간을 출력하는 함수 (0) | 2010.03.03 |
컬렉션 (0) | 2010.02.12 |
[JAVA] 컴파일러 들여다보기 (0) | 2009.12.24 |
자바에서 구조체사용.. (0) | 2009.12.23 |