Java

SSL 서버

by 마인드진 2009. 12. 29. 17:04

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를 참고하기 바란다.

[출처] SSL 서버|작성자 선성환