J2SE 1.4를 사용한 안전한 인터넷 프로그래밍 I

이 기사의 원문을 11월 java.sun.com 에서 보고 번역을 11월 말에 마친 후 번역물을 공개하는 것에 대한 허락을 구하기 위해 저자에게 메일을 보냈었다. 그러나 이 기사가 Sun에 저작권이 있는 모양인지 Sun에 문의를 하겠다고 했으며, 이후 Sun으로 부터 허락할 수 없다는 메일을 받았다.

그래서 완료된 번역물을 공개하지 않았다가 이번에 기사의 완결인 파트 II가 나와서 이왕 시작한 것이라 마져 번역을 하고 이렇게 공개를 한다. 어차피 이 번역물은 비 공식적인 것이며 비 상업용 목적으로 한 것이기 때문에 역자의 임의의 판단으로 공개하는 것이다.

전문적인 번역자도 아니며 공부의 목적으로 번역을 했기 때문에 오역이 있을 수 있다. 오역이 있더라도 너그럽게 이해하기 바라며 해당 오역에 대한 의견을 역자에게 보내주면 감사하겠다.


JavaTM 2, 일반판(J2SETM) 1.4를 사용한 안전한 인터넷 프로그래밍
파트 I: 서버 측
저자: Qusay H. Mahmoud
2002년 11월
역자: Barney Kim
원문: http://developer.java.sun.com/developer/technicalArticles/Security/secureinternet/

컴퓨터 네트워크 또는 인터넷을 통해 전송된 모든 정보는 가로채기 쉽다. 신용 카드 번호나 다른 개인 데이터와 같은 정보는 주의해야 한다. 인터넷이 보다 유용해지기 위해서는 사용자 정보를 보호해야 하는 애플리케이션과 같은 전자 상거래를 위해 암호와 인증, 그리고 보안 통신 프로토콜을 사용해야 한다. 보안 소켓 레이어(Secure Sockets Layer : SSL) 상의 HTTP는 보안 하이퍼텍스트 전송 프로토콜(secure Hypertext Transfer Protocol : HTTPS)는 이미 전자 상거래 애플리케이션에서 성공적으로 사용되고 있다.

자바 보안 소켓 확장(JavaTM Secure Socket Extension : JSSE)는 안전한 인터넷 통신을 가능하게 하는 자바 패키지 세트이다. JSSE는 프레임워크이며 보안 소켓 레이어(SSL)의 100% 순수한 자바 구현이다. 자바 개발자는 이들 패키지를 사용하여 TCP/IP 상의 HTTP, FTP, Telnet, NTTP와 같은 애플리케이션 프로토콜을 사용하는 클라이언트와 서버간에 데이터의 전송을 할 수 있는 기능을 가진 보안 네트워크 애플리케이션을 개발할 수 있다.

JSSE가 자바 2 SDK, 일반판, 버전 1.4 (Java 2 SDK, Standard Edition, version 1.4 : J2SETM 1.4)에 포함된 것은 좋은 소식이다. 즉, J2SE 1.4가 설치되어 있다면 어떤 추가적인 패키지를 다운로드 할 필요없이 SSL 기반하의 보안 인터넷 애플리케이션을 개발할 수 있다는 것이다. 이 기사는 2파트로 구성되어 있고, 현재와 미래 시장을 위한 보안 인터넷 애플리케이션을 어떻게 개발할 수 있는 지 설명하는 예제를 제공한다. 이 기사는 서버 측을 다루고 다음 기사는 클라이언트 측을 다룰 것이다. 이 기사는 SSL의 자세한 개관을 보여주면서 시작한다. 그리고 다음과 같이 보여줄 것이다:

  • JSSE API의 사용하기
  • 기존의 클라이언트-서버 애플리케이션에 SSL을 통합하기
  • 간단한 HTTP 서버 개발하기
  • HTTPS 요청을 처리하도록 HTTP 서버 변경하기
  • J2SE에 포함된 keytool을 사용하여 개발자 자신의 증명서 생성하기
  • 보안 HTTP 서버 개발, 설정, 실행하기

SSL 개관

SSL 프로토콜은 클라이언트 (일반적으로 웹 브라우저)와 HTTP 서버간에 보안 연결 상의 통신을 지원하기 위해 1994년 넷스케이프가 개발했다. SSL은 암호화, 소스 인증, 데이터 무결성을 제공한다. 이것은 안전하지 않은 공개된 네트워크 상에서 정보를 보호할 수 있다는 것을 의미한다. SSL은 여러 버전이 있다. SSL 2.0은 오늘날 보안에 취약하다. SSL 3.0은 일반적으로 지원된다. 마지막으로 전송 레이어 보안 (Transport Layer Security: TLS)은 SSL 3.0을 향상시키고 인터넷 표준으로 채택되었고 최근의 거의 모든 소프트웨어가 지원한다.

암호화(Encryption) 는 전송 전에 데이터를 완전히 무의미한 것으로 바꾸는 것으로 데이터를 인증되지 않은 사용으로부터 보호한다. 데이터는 한쪽 (클라이언트나 서버)에서 암호화된 후 전송되고 다른 측에서 복호화 한 후 데이터를 처리한다.

소스 인증(Source authentication) 은 데이터 전송자를 구별하기 위한 방법이다. 처음 브라우저나 다른 클라이언트는 보안 연결 상에서 웹 서버와 통신을 시도한다. 서버는 클라이언트에게 증명된 인증서(certificate)를 보여준다.

인증서는 인증 기관(certification authorities : CAs)로 알려진 믿을 만한 기관에 의해 발급되고 확인된다. 인증서는 개인의 신원을 공개-키로 표현한 것이다. 이것은 서명된 문서로 다음과 같은 것을 의미한다: 발급자는 문서의 공개키를 보증하며 발급자는 지정된 엔티티(entity)에 속해있다. 발급자 (인증 기관) 널리 알려진 인증기관은 Verisign, Entrust, 그리고 Thawte이다. 오늘날 SSL/TLS을 사용한 인증서는 X.509 인증서임을 주목하라.

데이터 무결성 (Data integrity)은 데이터가 전송된 이후 변경되지 않았음을 보장하는 것을 의미한다.

SSL과 TCP/IP 프로토콜 스택

보안 소켓 레이어란 이름이 의미하는 것처럼 SSL 연결은 TCP에 의해 연결된 소켓처럼 동작한다. 그래서 아래 그림 1에서 보여지듯이 SSL은 애플리케이션 레이어와 TCP 사이에 프로토콜 스택이 있기 때문에 SSL 연결을 보안 TCP 연결처럼 생각할 수 있다. 그러나 SSL이 out-of-band 데이터와 같은 TCP 기능 중 일부를 지원하지 않는다는 것을 주목해야 한다.


그림 1: SSL과 TCP/IP 프로토콜 스택

상호 교환 가능한(negotiable) 암호화

SSL 기능 중에 상호 교환 가능한 암호화와 인증 알고리즘을 지원으로 인해 보안 전자 상거래 트랜잭션에서 사실상의 표준 방법이 되었다. SSL 설계자들은 모든 협력업체가 동일한 클라이언트 소프트웨어를 사용하지 않고 그 결과로 모든 클라이언트가 어떤 특정 암호화 알고리즘을 지원할 수 없다는 것을 알고 있었다. 이것은 서버에도 마찬가지이다. 클라이언트와 서버 두 연결의 종단은 초기 핸즈쉐이크(handshake) 동안 암호화와 복호화 알고리즘(암호 슈트)을 상호 교환 가능하다. 만약 클라이언트와 서버에게 모두 알맞는 알고리즘이 없다고 판단되면 연결 시도는 실패할 것이다.

SSL이 클라이언트와 서버 서로 상호간의 인증을 지원한다는 것을 주목하라. 일반적으로 단지 서버가 SSL 레이어에서 인증된다. 클라이언트는 일반적으로 SSL로 보호되는 채널 상에서 암호를 전송하는 것으로 애플리케이션 레이어에서 인증된다. 이 방법은 은행에서, 주식 거래할때, 다른 보안 웹 애플리케이션에서 일반적인 방법이다.

SSL full 핸즈쉐이크 프로토콜은 그림 2로 표현되어있다. 이것은 SSL 핸드쉐이크 동안 교환된 메시지의 연속을 보여준다.


그림 2: SSL 핸드쉐이크 프로토콜

이들 메시지들은 다음과 같은 것을 의미한다:

  1. ClientHello: 클라이언트는 SSL 프로토콜 버전, 세션 아이디, 암호화 알고리즘과 지원하는 키 크기와 같은 암호 슈트와 같은 정보를 서버로 보낸다.
  2. ServerHello: 서버는 클라이언트와 서버가 모두 지원하는 최적의 암호 슈트를 선택하고 이 정보를 클라이언트로 보낸다.
  3. Certificate: 서버는 클라이언트에게 서버의 공개 키가 포함된 서버의 인증서를 보낸다. 이 메시지는 선택적이다. 이것은 서버 인증이 필요할 때 사용된다. 즉, 이것은 클라이언트가 서버를 확인하는 데 사용된다.
  4. Certificate Request: 서버가 클라이언트에게 클라이언트 인증을 요구할 때만 보낸다. 대부분의 전자 상거래 애플리케이션은 클라이언트 인증이 필요하지 않다.
  5. Server Key Exchange: 서버의 공개 키를 가지고 있는 인증서가 키 교환용으로 적합하지 않을때 보낸다.
  6. ServerHelloDone: 서버가 초기 상호 교환 처리를 완료했다고 클라이언트에게 보낸다.
  7. Certificate: 서버가 클라이언트에게 클라이언트 인증을 요구할 때만 보낸다.
  8. Client Key Exchange: 클라이언트와 서버 간에 공유하기 위한 보안 키를 클라이언트가 생성한다. Rivest-Shamir-Adelman (RSA) 암호화 알고르즘을 사용한다면 클라이언트는 서버의 공개 키를 사용하여 키를 암호화 한 후 서버로 보낸다. 서버는 메시지를 복호화하기 위해 서버의 개인 키나 보안 키를 사용한다. 그리고 공유된 보안 키를 꺼낸다. 이제 클라이언트와 서버는 한전하게 분산된 보안 키를 공유한다.
  9. Certificate Verify: If the server requested to authenticate the client, this message allows the server to complete the authentication process.
  10. Change Cipher Spec: 클라이언트는 서버에게 암호화된 모드로 교환할 것을 요청한다.
  11. Finished: 클라이언트는 서버에게 클라이언트가 보안 통신 준비가 되었다고 알린다.
  12. Change Cipher Spec: 서버는 클라이언트에게 암호화된 모드로 교환할 것을 요청한다.
  13. Finished: 서버는 클라이언트에게 서버가 보안 통신 준비가 되었다고 알린다. 이것은 SSL 핸드쉐이크의 끝을 의미한다.
  14. Encrypted Data: 클라이언트와 서버는 이제 보안 통신 채널 상에서 암호화된 메시지를 교환할 수 있다.

JSSE

자바 보안 소켓 확장 (JSSE)는 SSL과 TLS 프로토콜의 100% 순수한 자바 구현이며 프레임워크이다. JSSE는 데이터의 암호화 서버 인증, 메시지 무결성, 추가적인 클라이언트 인증을 위한 구조를 지원한다. JSSE의 매력은 기본적인 암호화 알고르즘을 추상화한 것이다. 그래서 어렵고 공격당하기 쉬운 보안 프로그램을 생성해야하는 위험을 최소화한다. 게다가 기존의 애플리케이션에 SSL을 매끄럽게 통합하는 것으로 보안 애플리케이션을 매우 간단하게 개발할 수 있다. JSSE 프레임워크는 SSL 2.0과 3.0, TLS 1.0과 같은 많은 다른 보안 통신 프로토콜을 지원할 수 있다. 그러나 J2SE v1.4.1 은 SSL 3.0과 TLS 1.0을 구현했다.

JSSE로 프로그래밍하기

JSSE API는 확장 네트워킹 소켓 클래스와 신뢰(trus)와 키 관리자, 소켓 생성 동작을 캡슐화하기 위한 소켓 팩토리 프레임워크를 제공하기 위해 java.securityjava.net 패키지를 가지고 있다. 이들 클래스는 javax.netjavax.net.ssl 패키지를 포함하고 있다.

SSLSocket과 SSLServerSocket

javax.net.ssl.SSLSocketjava.net.Socket클래스의 하위클래스이다.그래서 이 클래스는 모든 표준 Socket 메소드를 지원하고 소켓을 안전하게 하기 위해 특정 메소드를 추가했다. javax.net.ssl.SSLServerSocket 클래스는 서버 소켓을 생성하는 데사용된다는 것을 제외하고는 SSLSocket 클래스와 유사하다.

SSLSocket의 인스턴스를 생성하는 것은 다음 2가지 방법으로 할 수 있다:

  1. 클래스의 createSocket 메소드의 하나를 실행하는 것으로 SSLSocketFactory의 인스턴스 생성
  2. SSLServerSocketaccept 메소드를 통해서 생성

SSLSocketFactory와 SSLServerSocketFactory

javax.net.ssl.SSLSocketFactory 클래스는 보안 소켓을 생성하기 위한 객체 팩토리이다. 그리고 javax.net.ssl.SSLServerSocketFactory는 서버 소켓을 생성하기 위한 객체 팩토리이다.

SSLSocketFactory의 인스턴스는 다음 2가지 방법으로 생성할 수 있다:

  1. SSLSocketFactory.getDefault를 생성하는 것으로 기본 팩토리를 얻는 것으로 생성.
  2. 특별한 설정된 동작으로 새로운 팩토리를 만드는 것으로 생성.

기본 팩토리는 서버 인증만을 가능하도록 설정되어 있음을 주의한다.

기존의 클라이언트/서버 애플리케이션을 안전하게 만들기

기전의 클라이언트/서버 애플리케이션에 SSL을 통합하는 것을 통해 JSSE 코드 몇줄을 사용하여 쉽게 안전하게 만들 수 있다. 아래 예제에서 굵게 표시된 줄은 서버를 안전하게 만드는 데 필요한 코드이다:

import java.io.*;
import javax.net.ssl.*;

public class Server {
    int port = portNumber;
    SSLServerSocket server;
    try {
        SSLServerSocketFactory factory =
            (SSLServerSocketFactory)SSLServerSocketFactory.getDefault();
        server = (SSLServerSocket)factory.createServerSocket(portNumber);
        SSLSocket client = (SSLSocket)server.accept();

        // 입,출력 스트림을 생성한다.
        // 늘 그렇듯 출력 스트림을 통해 클라이언트로 보안 메시지를 보내고
        // 입력 스티림을 통해 클라이언트로 부터 보안 메시지를 받는다.
    } catch(Exception e) {
      //
    }
 }

아래 예제에서 굵게 표시된 줄은 클라이언트를 안전하게 만드는데 필요한 코드이다:

import java.io.*;
import javax.net.ssl.*;

public class Client {
    ...
    try {
        SSLSocketFactory factory = (SSLSocketFactory)SSLSocketFactory.getDefault();
        server = (SSLServerSocket)factory.createServerSocket(portNumber);
        SSLSocket client = (SSLSOcket)factory.createSocket(serverHost, port);

        // 입,출력 스트림을 생성한다.
        // 늘 그렇듯 출력 스트림을 통해 클라이언트로 보안 메시지를 보내고
        // 입력 스티림을 통해 클라이언트로 부터 보안 메시지를 받는다.
    } catch(Exception e) {
      //
    }
}

SunJSSE Provider

J2SE v1.4.1은 JSSE 제공자인 SunJSSE 를 포함하고 있다. 이 제공자는 자바 암호해독법 아키텍쳐 (Java Cryptography Architecture)로 설치되고 사전에 등록되어있다. SunJSSE 제공자는 구현의 이름으로 생각하면 된다. 이것은 가장 일반적인 SSL과 TLS 암호 슈트로써 SSL v3.0과 TLS v1.0의 구현을 제공한다. SSL의 구현(여기에서는 SunJSSE)을 통해 지원할 수 있는 암호 슈트의 목록은 SSLSocketgetSupportedCipherSuites 메소드를 호출하면 얻을 수 있다. 그러나 이들 암호 슈트 모두가 사용가능한 것은 아니다. 사용가능한 암호 슈트를 찾기위해서는 getEnabledCipherSuites 메소드를 호출한다. 이 목록은 setEnabledCipherSuites를 호출하여 변경할 수 있다.

완벽한 예제

저자는 JSSE를 사용할때 시스템 설정과 인증서와 키의 관리하는 것과 관련되어 대다수의 복잡한 문제를 찾았다. 이 예제를 통해 저자는 GET 요청 메소드를 지원하는 완벽한 HTTP 서버 애플리케이션을 어떻게 개발하고 설정하고 실행하는지를 보여줄 것이다.

HTTP 개관

하이퍼텍스트 전송 프로토콜(HTTP)는 요청에 반응하는 애플리케이션 프로토콜이다. 이 프로토콜은 GET, POST, PUT, DELETE 등과 같은 고정된 메소드 세트를 지원한다. GET 메소드는 일반적으로 웹 서버로부터 자원을 요청할 때 사용된다. 다음 2개는 간단한 GET 요청이다:

GET / HTTP/1.0
GET /names.html HTTP/1.0

안전하지 않은 HTTP 서버

HTTP 서버를 개발하기 위해 어떻게 HTTP 프로토콜이 동작하는지 이해해야 한다. 그러나 이 서버는 단순하게 오직 GET 요청 방식을 지원한다. 구현된 예제는 코드 예제 1이다. 이것은 멀티-쓰레드 HTTP 서버이다. ProcessConnection 클래스는 각각의 새로운 요청을 실행하기 위해 다른 쓰레드로 처리한다. 서버가 브라우저로부터 요청을 받으면 서버는 요청된 문서를 찾기 위해 요청을 분석한다. 만약 요청된 문서가 서버 상에 있다면 shipDocument 메소드는 서버로부터 요청된 문서를 보내진다. 만약 문서를 찾을 수 없다면 오류메시지가 서버로부터 보내진다.

코드 예제 1: HttpServer.java

import java.io.*;
import java.net.*;
import java.util.StringTokenizer;

/**
 * 이 클래스는 GET 요청 메소드를 지원하는 간단한 멀티쓰레드
 * HTTP 서버를 구현한다.
 * 서버는 8080 포트 번호를 사용해 클라이언트 요청을 기다리고 문서를
 * 서비스한다.
 */
public class HttpServer {
   // 서버가 사용할 포트 번호
   public static final int HTTP_PORT = 8080;

   public ServerSocket getServer() throws Exception {
      return new ServerSocket(HTTP_PORT);
   }

   // 멀티-쓰레딩 -- 각각의 요청을 위한 새로운 연결을
   // 생성한다.
   public void run() {
      ServerSocket listen;
      try {
         listen = getServer();
         while(true) {
            Socket client = listen.accept();
            ProcessConnection cc = new
              ProcessConnection(client);
         }
      } catch(Exception e) {
         System.out.println("Exception:
           "+e.getMessage());
      }
   }

   // 메인 프로그램
   public static void main(String argv[]) throws
     Exception {
      HttpServer httpserver = new HttpServer();
      httpserver.run();
   }
}

class ProcessConnection extends Thread {
   Socket client;
   BufferedReader is;
   DataOutputStream os;

   public ProcessConnection(Socket s) { // 생성자
      client = s;
      try {
         is = new BufferedReader(new InputStreamReader
           (client.getInputStream()));
         os = new DataOutputStream(client.getOutputStream());
      } catch (IOException e) {
         System.out.println("Exception: "+e.getMessage());
      }
      this.start(); // 여기서 쓰레드가 시작한다. start()는 run()을 호출할 것이다.
   }

   public void run() {
      try {
         // 요청을 받아 분석한다.
         String request = is.readLine();
         System.out.println( "Request: "+request );
         StringTokenizer st = new StringTokenizer( request );
            if ( (st.countTokens() >= 2) &&
              st.nextToken().equals("GET") ) {
               if ( (request =
                 st.nextToken()).startsWith("/") )
                  request = request.substring( 1 );
               if ( request.equals("") )
                  request = request + "index.html";
               File f = new File(request);
               shipDocument(os, f);
            } else {
               os.writeBytes( "400 Bad Request" );
            }
            client.close();
      } catch (Exception e) {
         System.out.println("Exception: " +
           e.getMessage());
      }
   }

  /**
   * 요청한 파일을 읽고 브라우저에 이것을 옮긴다.
   */
   public static void shipDocument(DataOutputStream out,
     File f) throws Exception {
       try {
          DataInputStream in = new
            DataInputStream(new FileInputStream(f));
          int len = (int) f.length();
          byte[] buf = new byte[len];
          in.readFully(buf);
          in.close();
          out.writeBytes("HTTP/1.0 200 OK");
          out.writeBytes("Content-Length: " + f.length());
          out.writeBytes("Content-Type:text/html");
          out.write(buf);
          out.flush();
       } catch (Exception e) {
          out.writeBytes("<html></head><body>");
          out.writeBytes("HTTP/1.0 400 " + e.getMessage());
          out.writeBytes("Content-Type: text/html");
          out.writeBytes("</body></html>");
          out.flush();
       } finally {
          out.close();
       }
   }
}

HttpServer 클래스를 테스트하기 위한 방법:

  1. HttpServer 코드 예제를 복사하고 HttpServer.java 선택한 디렉토리에 HttpServer.java 란 이름으로 저장한다.
  2. javac 를 사용해서 HttpServer.java 를 컴파일한다.
  3. Create some sample HTML files including “index.html”, which is the default document served in this example
  4. HttpServer를 실행한다. 서버는 8080 포트로 실행된다.
  5. 웹 브라우저를 열고 http://localhost:8080 나 http://127.0.0.1:8080/index.html과 같이 입력한다.

주의: HttpServer 가 어떤 악의적인 URL을 처리할 수 있다고 생각하는가? http://serverDomainName:8080/../../etc/passwdhttp://serverDomainName:8080//somefile.txt 와 같은 것은 어떤가? 예로 HttpServer 를 그런 URL을 허용하지 않도록 수정한다. 힌트: 개발자가 수정한 SecurityManagerjava.lang.SecurityManager 를 사용한다. main 메소드의 첫째줄에 System.setSecurityManager(new Java.lang.SecurityManager)문장을 추가하여 보안 관리자를 설치할 수 있다. 시도해보라.


HttpServer를 https:// URL을 처리할 수 있도록 확장하기

이제 HttpServer 클래스를 안전하게 만들기 위해 수정하자. 저자는 HTTP 서버가 https:// URL을 처리할 수 있도록 할 것이다. 저자가 이전에 언급한 것처럼 JSSE를 사용하면 애플리케이션에 SSL을 매우 쉽게 통합할 수 있다.

서버 인증서를 생성하기

이전에 언급한 것과 같이 SSL은 인증을 위해 인증서를 사용한다. 인증서는 클라이언트와 서버를 위해 생성해야 하며 이것은 SSL을 사용한 안전한 통신에 필요하다. JSSE는 J2SE에 포함된 자바 keytool 을 사용하여 인증서를 생성한다. 저자는 HTTP 서버요으로 RSA 인증서를 생성하기 위해 아래 명령을 실행했다.

프롬프트> keytool -genkey -keystore serverkeys -keyalg rsa -alias qusay

이 명령은 별명 qusay 이란 신원으로 인증서를 생성할 것이다. 그리고 인증서는 serverkeys 파일명으로 저장될 것이다. 툴은 인증서를 생성하기 위해 정보를 입력할 것을 요구한다. 저자가 입력한 정보는 굵게 강조했다:

Enter keystore password:  hellothere
What is your first and last name?
  [Unknown]:  ultra.domain.com
What is the name of your organizational unit?
  [Unknown]:  Training and Consulting
What is the name of your organization?
  [Unknown]:  javacourses.com
What is the name of your City or Locality?
  [Unknown]:  Toronto
What is the name of your State or Province?
  [Unknown]:  Ontario
What is the two-letter country code for this unit?
  [Unknown]:  CA
Is CN=ultra, OU=Training and Consulting,
O=javacourses.com, L=Toronto, ST=Ontario, C=CA correct?
  [no]:  yes

Enter key password for
        (RETURN if same as keystore password):  hiagain

여러분이 본 것처럼 keytool 은 저자에게 서버가 키 저장소에 접근할때 서버가 알고 있어야 하는 키 저장소를 위한 암호를 입력하기를 요구했다. 또한 keytool은 별명(alias) 용 암호를 입력하길 요구했다. 원한다면 이와 같은 암호 정보는 -storepass-keypass 옵션을 사용하여 keytool 명령으로 지정할 수 있다. 저자는 이름으로 “ultra.domain.com” 를 사용했음을 주목하라. 이 이름은 저자의 시스템의 가상의 이름이다. 여러분은 서버의 호스트이름이나 IP 주소를 입력해야 한다.

keytool 명령을 실행할 때 여러분의 시스템 속도에 따라 인증서를 생성하는데 수초가 걸릴 수 있다.

본인의 서버용 인증서를 생성한 후 이 인증서를 확인하기 위해 HttpServer를 수정할 수 있다. 만약 HttpServer 클래스를 시험하려면 getServer 메소드가 서버 소켓을 리턴하는데 사용된다는 것을 주목해야 한다. 보안 서버 소켓을 리턴하도록 수정해야 할 메소드는 단지 getServer 메소드라는 것이다. 이런 변경사항은 코드 예제 2에서 굵게 표시되었다. 저자는 포트 번호를 443으로 변경했음을 주의하라. 이 포트는 HTTPS용 기본 포트 번호이다. 포트 번호 0에서 1023까지는 예약되어 있음을 주목해야한다. 만약 HttpsServer가 다른 포트 번호로 실행하려면 URL은 https://localhost:포트번호가 되어야 한다. 포트 번호 443 으로 실행한다면 URL은 https://localhost 이다.

코드 예제 2: HttpsServer.java

import java.io.*;
import java.net.*;
import javax.net.*;
import javax.net.ssl.*;
import java.security.*;
import java.util.StringTokenizer;

/**
 * 이 클래스는 GET 요청 메소드를 지원하는 간단한 멀티쓰레드
 * HTTP 서버를 구현한다.
 * 서버는 443 포트 번호를 사용해 클라이언트 요청을 기다리고 문서를
 * 서비스한다.
 */
public class HttpsServer {
   String keystore = "serverkeys";
   char keystorepass[] = "hellothere".toCharArray();
   char keypassword[] = "hiagain".toCharArray();

   // 서버가 사용할 포트 번호
   public static final int HTTPS_PORT = 443;

   public ServerSocket getServer() throws Exception {
      KeyStore ks = KeyStore.getInstance("JKS");
      ks.load(new FileInputStream(keystore), keystorepass);
      KeyManagerFactory kmf =
        KeyManagerFactory.getInstance("SunX509");
      kmf.init(ks, keypassword);
      SSLContext sslcontext =
        SSLContext.getInstance("SSLv3");
      sslcontext.init(kmf.getKeyManagers(), null, null);
      ServerSocketFactory ssf =
        sslcontext.getServerSocketFactory();
      SSLServerSocket serversocket = (SSLServerSocket)
        ssf.createServerSocket(HTTPS_PORT);
      return serversocket;
    }

   // 멀티-쓰레딩 -- 각각의 요청을 위한 새로운 연결을
   // 생성한다.
   public void run() {
      ServerSocket listen;
      try {
         listen = getServer();
         while(true) {
            Socket client = listen.accept();
            ProcessConnection cc = new
              ProcessConnection(client);
         }
      } catch(Exception e) {
         System.out.println("Exception: "+e.getMessage());
      }
   }

   // 메인 프로그램
   public static void main(String argv[]) throws Exception {
      HttpsServer https = new HttpsServer();
      https.run();
   }
}

위에서 다음의 3개 줄을 보자:

String keystore = "serverkeys";
char keystorepass[] = "hellothere".toCharArray();
char keypassword[] = "hiagain".toCharArray();

이것은 각각 키 저장소의 이름과 키 저장소의 암호, 키 암호를 지정한다. 그러나 코드상에 암호를 넣는 것은 상품(프러덕션) 코드를 위해서는 좋은 생각이 아니다. 이것들은 서버를 실행하는 명령으로 지정할 수 있다.

getServer 메소드에 JSSE 관련 코드의 나머지는 다음과 같다:

  • 서버는 serverkeys 키 저장소에 접근한다. JKS는 keytool에 의해 생성되는 키 저장소의 한 형태인 자바 키 저장소 (Java KeyStore)이다.
  • KeyManagerFactory는 키 저장소를 위한 X.509 키 관리자를 생성하는데 사용된다.
  • SSLContext는 구현된 JSSE를 위한 환경이다. 이것은 SSLServersocket를 생성하기 위해 ServerSocketFactory를 생성한다. SSL 3.0으로 지정하더라도 TLS 1.0과 같은 다른 프로토콜 버전을 지원하도록 구현이 될것이다. 그러나 이전 브라우저들은 SSL 3.0을 보다 널리 사용한다.

기본적으로 클라이언트 인증은 필요하지 않다는 것을 주의하라. 만약 서버가 클라이언트 인증을 요구하도록 하려면 serversocket.setNeedClientAuth(true)를 사용한다. .

HttpsServer 클래스를 테스트하려면:

  1. HttpsServerProcessConnection 클래스를 HttpsServer.java 라는 이름으로 저장한다.
  2. keytool로 생성한 serverkeys 파일이 있는 디렉토리에 이 파일을 저장한다.
  3. javac를 사용하여 HttpsServer.java를 컴파일 한다.
  4. HttpsServer를 실행한다. 서버는 기본적으로 443 포트로 동작한다. 만약 443 포트로 실행할 수 없다면 1024 보다 큰 포트 번호를 선택한다.
  5. 브라우저를 열고 https://localhost 나 https://127.0.0.1 요청을 입력한다. 이것은 서버가 443 포트로 동작한다고 가정할 때이고 만약 포트가 다르다면 https://localhost:포트번호 를 입력한다.

브라우저에서 https:// URL을 입력하면 그림 3과 같은 보안 경고 팝업 창을 볼 수있다. 이것은 HTTP 서버 인증서가 자체 발급된 것이기 때문이다. 즉 브라우저가 저장소에 보관하고 있는 인증 기관 목록에서 찾을 수 없는 알려지지 않은 인증 기관에서 발급받았다는 것이다. 인증서를 본 후 (인증서가 올바른지를 확인하고 어디서 인증서를 발급했는지 확인한다) 인증서를 설치할 수 있고, 인증서를 거부하거나 허용할 수 있는 선택권이 있다.

그림 3
그림 3: 알려지지 않은 인증 기관으로 부터 발급받은 서버 인증서


주의: 자체 인증서를 생성하는 것은 내부 사설 시스템을 위해 괜찮다. 하지만 공개 시스템을 위해서는 브라우저 보안 경고를 방지하기 위해 공인 인증 기관에서 인증서를 발급받는 것이 좋다.


만약 인증서를 허용한다면 보안 연결 하에서 페이지가 보일 것이다. 이후에 동일한 웹 사이트에 접속하는 것에 대해서는 보안 경고가 나타나지 않는다. 많은 웹 사이트들이 스스로 발급하거나 알려지지 않은 인증 기관에서 발급받은 인증서를 가지고 HTTPS를 사용한다는 것을 주목하라. 예를 들면 https://www.jam.ca에 접속해보라. 이 웹사이트에 방문한 적이 없다면 그림 3과 같은 보안 경고를 보게될 것이다.


주의: 인증서를 허용한다면 이것은 오직 하나의 세션에서만 가능하다. 즉, 브라우저를 완전히 종료하면 인증서는 없어진다. 넷스케이프(Netscape)와 마이크로소프트 인터넷 익스플로러 (Microsoft Internet Explorer : MS IE)는 인증서를 영구적으로 설치할수 있도록 지원한다. MS IE에서 이렇게 하려면 그림 3에서 “인증서 보기”를 선택하고 “인증서 설치”를 선택한다.


결론

이 기사는 SSL의 자세한 개관을 보여주고 JSSE 프레임워크와 구현을 설명했다. 이 기사의 예제들은 SSL을 클라이언트/서버 애플리케이션에 통합하는 것이 얼마나 쉬운 지를 보여주고 있다. 이 기사는 테스트 목적으로 사용할 수 있는 보안 HTTP 서버를 보여주었다. JSSE API와 HTTPS 요청을 처리할 수 있는 브라우저에 대한 더 많은 정보는 조정을 위해 기다려라.

추가 정보

감사의 말

이 기사를 개선할 수 있도록 도움말을 준 썬 마이크로시스템즈의 Andres Sterbenz에게 매우 감사하다.

저자에 대하여

Qusay H. Mahmoud 는 자바 컨설팅과 교육 서비스를 제공한다. 그는 자바에 관한 여러 기사를 썼고 자바를 사용한 분산 프로그래밍 Distributed Programming with Java (Manning Publications, 1999)과 무선 자바 배우기 Learning Wireless Java (O’Reilly, 2002)의 저자이다.