Programming/C

SSL Programming Tutorial

Tribal 2018. 3. 18. 14:24

원본 글 : http://h41379.www4.hpe.com/doc/83final/ba554_90007/ch04s03.html#ssl-app-fig


번역 안 하려다가 볼게 많아서 결국 한다... 원본 글 내부에는 좀 더 자세한 URL 링크가 존재한다.

---------------------------------------------------------------------------------------------------------------------------------------

  이 섹션에서는 OpenSSL API를 사용하는 간단한 SSL 클라이언트와 서버 프로그램의 구현을 설명한다.


  SSL 클라이언트와 서버 프로그램은 설정과 구성이 달라질 수 있긴 하지만, 보통 내부 절차는 그림 4-8 "OpenSSL API를 사용한 SSL 어플리케이션 개요"와 같이 이루어진다. 이와 같은 절차는 다음 섹션에서 설명하고자 한다.


그림 4-8 OpenSSL API를 사용한 SSL 어플리케이션 개요

  1. 초기화 : SSL 라이브러리의 암호, 알고리즘 등을 로드하여 초기화 하는데 사용
  2. SSL 방식 선택 : SSLv2, SSLv3, TLSv1
  3. SSL_CTX 생성 : SSL Context Structure를 생성
  4. SSL_CTX 설정 : 생성된 SSL Context Structure에 키, 인증서 등이 설정됨
  5. SSL 생성
  6. TCP/IP 소켓 설정
  7. BIO 생성 및 설정
  8. SSL Handshake
  9. SSL Data 통신
    • 상황에 따라 SSL Rehandshake 수행
  10. SSL 종료
    • 상황에 따라 SSL 세션 재사용 수행


SSL Library 초기화


  SSL 어플리케이션 프로그램에서 OpenSSL API를 호출하기 위해서는, 다음과 같은 SSL API를 사용하여 초기화를 수행해야 한다.

1
2
SSL_library_init();       /* SSL 통신을 위해 암호와 해시 알고리즘 로드 */                
SSL_load_error_strings(); /* 에러 기록을 남기기 위한 에러 관련 문자열 로드 */
cs


  SSL_library_init() API는 SSL의 API에 사용되는 모든 암호, 해시 알고리즘을 등록한다. 로드되는 암호화 알고리즘으로는 DES-CBC, DES-EDE3-CBC, RC2, RC4가 있으며, 해시 알고리즘으로는 MD2, MD5, SHA가 있다. SSL_library_init() API는 항상 1을 반환한다.


  SSL 어플리케이션은 SSL_load_error_strings() API를 호출해야 한다. 이 API는 SSL API와 Crypto API를 위해 에러 관련 문자열을 로드한다. 많은 SSL 어플리케이션이 Crypto API와 SSL API를 호출하고 있기 때문에, SSL과 Crypto 양 쪽의 에러 문자열은 로드될 필요가 있다.



SSL Context Structure(SSL_CTX) 생성 및 설정


  초기화 단계를 거친 후, 먼저 SSL/TLS 프로토콜의 버전을 선택한다. 다음의 API 중 하나를 사용하여 SSL_METHOD 구조체를 생성함으로써, 프로토콜의 버전을 선택한다. 그런 다음 SSL_METHOD 구조체는 SSL_CTX_new() API를 사용함으로써 SSL_CTX 구조체를 생성하는데 사용된다.


  모든 SSL/TLS 버전을 위해, SSL_METHOD 구조체를 생성하는 3가지 타입의 API가 있다. 양 쪽 클라이언트와 서버를 위한 API, 오로지 서버만을 위한 API, 오로지 클라이언트만을 위한 API이다. SSLv2, SSLv3, TLSv1 API는 같은 이름의 프로토콜이다. 표 4-2 SSL_METHOD 생성을 위한 타입별 API 에서 타입별 API를 볼 수 있다.


표 4-2 SSL_METHOD 생성을 위한 타입별 API

 프로토콜 타입

 클라이언트와 서버 혼합

 서버 전용

 클라이언트 전용

 SSLv2

 SSLv2_method()

 SSLv2_server_method()

 SSLv2_client_method()

 SSLv3

 SSLv3_method()

 SSLv3_server_method()

 SSLv3_client_method()

 TLSv1

 TLSv1_method()

 TLSv1_server_method()

 TLSv1_client_method()

 SSLv23

 SSLv23_method()

 SSLv23_server_method()

 SSLv23_client_method()

※ 참고 : SSLv23은 SSL 프로토콜 버전이 없다. SSLv23_method() API와 전용 SSLv23 API는 peer와의 호환성을 위해 SSLv2, SSLv3, TLSv1을 선택한다.


  SSL 클라이언트/서버 어플리케이션을 개발할 때, SSL/TLS 버전간의 호환성이 이루어지지 않을 수 있다는 것을 고려해야 한다. 예를 들어, TLSv1 서버는 SSLv2나 SSLv3 클라이언트로부터 받은 client-hello 메시지를 이해할 수 없다. SSLv2 클라이언트/서버는 오로지 SSLv2 peer로부터 받은 메시지만을 이해할 수 있다. SSLv23_method() API와 전용 SSLv23 API는 peer와의 호환성을 중요시 할 때 사용 가능하다. SSLv23_method를 사용한 SSL 서버는 SSLv2, SSLv3, TLSv1의 hello message들을 전부 이해할 수 있다. 하지만, SSLv23_method를 사용한 SSL 클라이언트는 SSLv2 hello 메시지를 전송하기 때문에 SSLv3/TLSv1_method를 사용하는 SSL 서버와의 연결은 성립시킬 수 없다.


  SSL_CTX_new() API는 인자로 SSL_METHOD 구조체를 가지고, SSL_CTX 구조체를 생성한다.


  아래의 예제에서는, SSLv3 클라이언트와 SSLv3 서버 중 하나에 사용될 수 있는 SSL_METHOD 구조체가 생성되고 SSL_CTX_new()에 사용된다. 이렇게 생성된 SSL_CTX 구조체는 SSLv3 클라이언트와 서버를 위해 초기화된다.

1
2
meth = SSLv3_method();
ctx = SSL_CTX_new(meth);
cs



인증서와 키 설정


  "SSL 어플리케이션을 위한 인증서"는 SSL 클라이언트와 서버 프로그램에 적합한 인증서를 어떻게 설정하는지에 대해서 설명한다. 이 설정은 SSL_CTX와 SSL 구조체 내부에 인증서와 키를 로드함으로써 끝난다. 필수인 인증서와 선택 사항인 인증서는 다음과 같다:

  • SSL 서버용:
    서버 소유의 인증서 (필수)
    CA 인증서 (선택)
  • SSL 클라이언트용:
    CA 인증서 (필수)
    클라이언트 소유의 인증서 (선택)


인증서 로딩 (클라이언트/서버 인증서)


  SSL_CTX 구조체 내부에 인증서를 로드하기 위해서는 SSL_CTX_use_certificate_file() API를 사용하면 된다. SSL 구조체 내부에 인증서를 로드하기 위해서는 SSL_use_certificate_file() API를 사용하면 된다. SSL 구조체를 생성될 때, SSL 구조체에는 자동적으로 SSL_CTX 구조체에 저장된 인증서가 로드된다. 따라서 필요하다면 SSL 구조체를 대상으로 SSL_use_certificate_file() API를 호출하면 된다. 만일 다른 인증서를 로드할 필요가 있는 경우는, SSL_CTX 구조체 내부에 인증서가 저장된 다음에 다른 인증서를 로드한다.



기본키 로딩


  다음 단계는 서버나 클라이언트 인증서에 부합하는 개인키를 세팅하는 것이다. SSL handshake에서, 암호화를 위해 peer가 인증서를 사용할 수 있도록 인증서(공개키가 포함된)가 전송된다. peer로부터 전송된 메시지는 오로지 개인키만을 사용하여 복호화할 수 있다. 따라서 SSL 구조체 내부에 공개키를 사용하여 생성된 개인키를 사전에 생성해둬야 한다.


SSL이나 SSL_CTX 구조체 내부에 개인키를 로드할 수 있는 API 목록:

  • SSL_CTX_use_PrivateKey()
  • SSL_CTX_use_PrivateKey_ASN1()
  • SSL_CTX_use_PrivateKey_file()
  • SSL_CTX_use_RSAPrivateKey()
  • SSL_CTX_use_RSAPrivateKey_ASN1()
  • SSL_CTX_use_RSAPrivateKey_file()
  • SSL_use_PrivateKey()
  • SSL_use_PrivateKey_ASN1()
  • SSL_use_PrivateKey_file()
  • SSL_use_RSAPrivateKey()
  • SSL_use_RSAPrivateKey_ASN1()
  • SSL_use_RSAPrivateKey_file()


CA 인증서 로딩


  인증서를 검증하기 위해서는, 먼저 CA 인증서를 로드해야 한다. 왜냐하면 peer의 인증서는 CA 인증서에 대해 검증되기 때문이다. SSL_CTX_load_verify_locations() API는 SSL_CTX 구조체 내부에 CA 인증서를 로드한다.


API의 원형은 다음과 같다:

1
int SSL_CTX_load_verify_locations(SSL_CTX *ctx, const char *CAfile, const char *CApath);
cs


  첫 번째 인자인 ctx는 CA 인증서가 내부에 로드되어 있는 SSL_CTX 구조체를 가리킨다. 두 번째와 세 번째 인자인 CAfile과 CApath는 CA 인증서의 위치를 명시하는데 사용된다. CA 인증서를 찾을 때, OpenSSL 라이브러리는 먼저 CAfile에 있는 인증서를 탐색한 후, 그 다음으로 CApath를 탐색한다.


다음은 CAfile과 CApath 인자에 적용되는 룰이다:

  • CAfile을 통해 인증서가 명시(인증서는 SSL 어플리케이션과 같은 디렉토리에 존재해야 함)된 경우, CApath에는 NULL을 지정해준다.
  • 세 번째 인자인 CApath를 사용하면, CAfile에 NULL을 지정해준다. 또한 CApath로 명시된 디렉토리에는 CA 인증서를 해시화해야 한다. 해시화를 수행하기 위해서는 챕터 3에 설명되어 있는 인증서 툴을 사용하면 된다.


Peer 인증서 검증 설정


  SSL_CTX 구조체에 로드되는 CA 인증서는 peer 인증서 검증을 위해 사용된다. 예를 들어, SSL 클라이언트에 로드되는 CA 인증서와 서버의 인증서 사이에서 신뢰성을 확인하는데 SSL 클라이언트에서 peer 인증서 검증을 수행한다.


  성공적으로 검증을 하기 위해서, peer 인증서는 CA 인증서를 직접적이나 간접적(적절한 인증서 체인이 존재하는 경우)으로 서명되어야 한다. CA 인증서로부터 peer 인증서까지의 인증서 체인의 길이는 SSL_CTX와 SSL 구조체의 verify_depth 필드에 세팅되어야 한다. (SSL의 값은 SSL_new() API를 사용하여 SSL 구조체를 생성할 때, SSL_CTX로부터 상속된다.) verify_depth를 1이 세팅된다면, peer 인증서가 CA 인증서를 통해 직접 서명되어야 한다는 것을 나타낸다.


  SSL_CTX_set_verify() API는 SSL_CTX 구조체에 검증 flag와 세 번째 인자인 커스텀마이즈한 검증을 위해 callback 함수를 세트함으로써 허가를 한다. (callback 함수에 NULL로 설정하면 기본적인 검증 함수를 사용되게 빌드하겠다는 것을 의미함) SSL_CTX_set_verify()의 두 번째 인자에는, 다음과 같은 macro들을 세팅할 수 있다:

  • SSL_VERIFY_NONE
  • SSL_VERIFY_PEER
  • SSL_VERIFY_FAIL_IF_NO_PEER_CERT
  • SSL_VERIFY_CLIENT_ONCE

  SSL_VERIFY_PEER macro는 SSL 클라이언트와 서버 양 쪽의 검증을 설정하는데 사용될 수 있다. 그러나 후속 동작은 클라이언트나 서버에 세팅된 macro 여부에 따라 결정된다. 예를 들면 다음과 같다:

1
2
3
4
5
/* peer 인증서를 위해 callback function (verify_callback) 세팅 */
/* 검증 */
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, verify_callback);
/* verification depth를 1로  */
SSL_CTX_set_verify_depth(ctx,1);
cs


  SSL_get_verify_result() API를 사용함으로써, 일반적이지 않는 방법으로 peer의 인증서를 검증하는 것도 가능하다. 이 방법은 SSL_CTX_set_verify() API를 사용하지 않고 peer 인증서의 검증 결과를 얻을 수 있도록 허가한다.


  SSL_get_verify_result() API를 호출하기 전, 다음과 같은 2개의 API가 호출된다:

  1. SSL handshake를 수행하기 위해서 SSL_connect() (클라이언트의 경우)나 SSL_accept() (서버의 경우)를 호출한다. handshake를 수행하는 동안 인증서 검등이 수행되게 된다. SSL_get_verify_result()는 검증 수행 전에는 결과를 얻을 수 없다.
  2. peer의 인증서를 명시적으로 얻기 위해 SSL_get_peer_certification()을 호출한다. X509_V_OK macro의 값은 검증에 성공했거나 peer의 인증서가 존재하지 않는 경우에 리턴된다.

  다음의 코드는 SSL 클라이언트에서 SSL_get_verify_result()를 어떻게 사용하는지를 보여준다:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
SSL_CTX_set_verify_depth(ctx, 1);
    err = SSL_connect(ssl);
if(SSL_get_peer_certificate(ssl) != NULL)
    {
        if(SSL_get_verify_result(ssl) == X509_V_OK)
            BIO_printf(bio_c_out, "client verification with SSL_get_verify_result() succeeded.\n");                
        else{
            BIO_printf(bio_err, "client verification with SSL_get_verify_result() failed.\n");
 
            exit(1);
        }
    }
    else
      BIO_printf(bio_c_out, -the peer certificate was not presented.\n-);
cs



예제 1: SSL 서버를 위해 인증서 설정


  SSL 프로토콜을 사용하기 위해서는 서버에 서버가 소유한 인증서와 키를 세팅해야 한다. 서버에서 클라이언트의 인증서를 사용하여 클라이언트의 인증을 하고자 하는 경우, 서버는 클라이언트의 인증서를 검증할 수 있는 CA 인증서를 로드해야 한다.


  다음의 예제는 SSL 서버를 위해 어떻게 인증서를 세팅하는 지를 보여준다:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* SSL context에 서버의 인증서를 로드 */
if (SSL_CTX_use_certificate_file(ctx, SERVER_CERT, SSL_FILETYPE_PEM) <= 0) {
    ERR_print_errors(bio_err);  /* == ERR_print_errors_fp(stderr); */
    exit(1);
}
 
/* SSL context에 서버의 개인키를 로드 */
if (SSL_CTX_use_PrivateKey_file(ctx, SERVER_KEY, SSL_FILETYPE_PEM) <= 0) {
    ERR_print_errors(bio_err);  /* == ERR_print_errors_fp(stderr); */
    exit(1);
}
 
/* 신뢰할 수 있는 CA 로드 */
if (!SSL_CTX_load_verify_locations(ctx,CA_CERT,NULL)) { 
    ERR_print_errors(bio_err);  /* == ERR_print_errors_fp(stderr); */
    exit(1);
}
 
/* peer (클라이언트)의 인증서를 검증하도록 세트 */
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, verify_callback);
/* verrification depth를 1로 세트 */
SSL_CTX_set_verify_depth(ctx,1);
cs



예제 2: SSL 클라이언트를 위해 인증서 설정


  일반적으로, SSL 클라이언트는 SSL handshake를 수행하는 과정에서 서버의 인증서를 검증한다. 이 검증에서 클라이언트는 신뢰가능한 CA 인증서가 설정되어 있어야 한다. 서버의 인증서는 SSL 클라이언트에 서버의 인증서 검증을 성공적으로 하기 위해 로드된 CA 인증서를 사용하여 서명되어야 한다.


  다음은 SSL 클라이언트를 위해 인증서를 어떻게 설정하는지를 보여주는 예제이다:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*----- SSL_CTX 구조체에 클라이언트 인증서를 로드 -----*/
if(SSL_CTX_use_certificate_file(ctx,CLIENT_CERT, SSL_FILETYPE_PEM) <= 0){
    ERR_print_errors_fp(stderr);
    exit(1);
}
 
/*----- SSL_CTX 구조체에 개인키를 로드 -----*/
if(SSL_CTX_use_PrivateKey_file(ctx,CLIENT_KEY, SSL_FILETYPE_PEM) <= 0){
    ERR_print_errors_fp(stderr);
    exit(1);
}
 
/* 신뢰가능한 CA를  */
if (!SSL_CTX_load_verify_locations(ctx,CA_CERT,NULL)) {
    ERR_print_errors_fp(stderr);
    exit(1);
}
cs



SSL 구조체를 생성하고 설정


  SSL 구조체를 생성하기 위해서는 SSL_new()를 호출하면 된다. SSL 구조체에는 SSL 연결을 위한 정보가 저장되게 된다. SSL_new() API를 위한 프로토콜은 다음과 같다:

1
ssl = SSL_new(ctx);
cs


  새롭게 생성된 SSL 구조체는 SSL_CTX 구조체로부터 정보를 상속받는다. 이 정보에는 연결 방법의 타입, 옵션, 검증에 대한 설정, 타임아웃 설정이 포함된다. 만약 SSL_CTX 구조체에 적절한 초기화와 설정이 제대로 이루어 졌다면, SSL 구조체를 위해서 별도의 추가 설정이 필요하지 않는다.


  SSL API를 사용하여 SSL 구조체의 기본 값을 수정할 수 있다. 이렇게 하면, SSL_CTX 구조체의 속성이 세팅된 API의 변형을 사용하는게 가능하다. 예를 들어, SSL_CTX 구조체 내부에 인증서를 로드하기 위하여 SSL_CTX_use_certificate()를 사용 가능하다. 그리고 또한 SSL 구조체 내부에 인증서를 로드하기 위하여 SSL_use_certificate()를 사용 가능하다.



TCP/IP 연결 설정


  대게 SSL은 일부 연동가능한 프로토콜과 함께 동작하는데, 그 중에서도 TCP/IP는 SSL을 사용하는 가장 많이 사용되는 통신 프로토콜이다.


  다음의 섹션에서는 SSL API를 사용하기 위해 어떻게 TCP/IP를 설정하는지 설명한다. 이 설정은 많은 TCP/IP 클라이언트/서버 어플리케이션 프로그램과 동일하며, SSL API을 사용하는 어플리케이션 프로그램에만 한정되지 않는다. 이 섹션에서, TCP/IP는 일반적인 socket AIP를 사용하여 설정되지만, OpenVMS 시스템 서비스를 사용하는 것도 가능하다.



Listening Socket 생성 및 설정 (SSL Server)


  SSL 서버는 일반적인 TCP/IP 서버로써 2개의 소켓이 필요하다. - SSL 연결을 위한 소켓과 SSL 클라이언트로부터 오는 연결 요청을 감지하기 위한 소켓이다.


  다음의 코드에서, socket() 함수는 listening socket을 생성한다. 그 다음에 bind()를 사용하여 주소와 포트를 listening socket에 배치하고, listen() 함수로 클라이언트로부터 오는 TCP/IP 연결 요청을 listening socket에 처리할 수 있도록 허가한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 listen_sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
 CHK_ERR(listen_sock, "socket");
 
 memset(&sa_serv, 0sizeof(sa_serv));
 sa_serv.sin_family      = AF_INET;
 sa_serv.sin_addr.s_addr = INADDR_ANY;
 sa_serv.sin_port        = htons(s_port);      /* Server Port number */
 
 err = bind(listen_sock, (struct sockaddr*)&sa_serv,sizeof(sa_serv));
 CHK_ERR(err, "bind");
 
 /* TCP 요청을 받음 */
 err = listen(listen_sock, 5);
 CHK_ERR(err, "listen");
cs



Socket 생성 및 설정(SSL Client)


  클라이언트에서는 TCP/IP socket를 생성하고, 이 socket을 사용하여 서버에 연결을 시도해야 한다. 지정된 서버와 연결을 성립시키는데에는, TCP/IP connect() 함수가 사용된다. 해당 함수가 성공적으로 처리되는 경우, socket은 연결 이외에도 데이터 통신에 사용될 수 있도록 첫 번째 인자로써 connect() 함수를 거치게 된다.

1
2
3
4
5
6
7
8
9
10
 sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
 CHK_ERR(sock, "socket");
 
 memset (&server_addr, '\0'sizeof(server_addr));
 server_addr.sin_family      = AF_INET;
 server_addr.sin_port        = htons(s_port);       /* Server Port number */
 server_addr.sin_addr.s_addr = inet_addr(s_ipaddr); /* Server IP */
 
 err = connect(sock, (struct sockaddr*&server_addr, sizeof(server_addr));
 CHK_ERR(err, "connect");
cs



TCP/IP 연결 성립 (SSL Server)


  연결 요청이 오는 것을 허용하고 TCP/IP 연결을 성립시키기 위해서는, SSL 서버는 accept() 함수를 호출할 필요가 있다. accept() 함수는 SSL 클라이언트와 서버 사이의 데이터 통신을 위해 사용될 수 있게 socket을 생성되게 된다. 예를 들어:

1
2
3
 sock = accept(listen_sock, (struct sockaddr*)&sa_cli, &client_len);
 BIO_printf(bio_c_out, "Connection from %lx, port %x\n"
 sa_cli.sin_addr.s_addr, sa_cli.sin_port);
cs



Socket/SSL 구조체 내부의 Socket BIO 설정


  SSL 구조체와 TCP/IP socket(sock)을 생성한 후에, socket을 통하여 자동적으로 SSL 구조체가 수행될 수 있도록 SSL 데이터 통신을 위해 설정해 주어야 한다.

  다음의 코드들은 ssl에 sock을 배치하는 다양한 방법 들이다. 가장 간단한 방법은 SSL 구조체 내부에 직접 소켓을 세팅하는 방법인데, 다음과 같다:

1
SSL_set_fd(ssl, sock);
cs


  조금 더 좋은 방법은 OpenSSL을 통해 I/O를 추상적으로 제공하는 BIO 구조체를 사용하여 것이다. BIO는 기본적인 I/O의 세부적인 정보를 은폐하기 때문에 이 방법이 더 선호된다. BIO 구조체가 제대로 세팅되어 있다면, I/O를 통해 SSL 연결을 성립시키는게 가능하다.


  다음의 두 예제는 어떻게 socket BIO를 생성하고 SSL 구조체 내부에 세팅하는지 나타낸다.

1
2
3
 sbio=BIO_new(BIO_s_socket());
 BIO_set_fd(sbio, sock, BIO_NOCLOSE);
 SSL_set_bio(ssl, sbio, sbio);
cs


  다음의 예제에서, BIO_new_socket() API는 TCP/IP socket에 배치된 socket BIO를 생성하고, SSL_set_bio() API는 SSL 구조체 내부에 socket BIO를 배치한다. 다음의 코드 두 줄은 위의 세 줄과 동일하게 처리된다.

1
2
 sbio = BIO_new_socket(socket, BIO_NOCLOSE);
 SSL_set_bio(ssl, sbio, sbio);
cs

※ 참고 : BIO가 이미 ssl에 연결된 경우, BIO_free()가 호출된다(읽는 쪽과 쓰는 쪽 모두).



SSL Handshake


  SSL handshake는 통신에 중요한 암호키를 교환하는 과정을 수해아는 복잡한 프로세스이다. 따라서, handshake는 SSL 서버에서는 SSL_accept(), SSL 클라이언트에서는 SSL_connect()를 호출함으로써 완료될 수 있다.



SSL 서버에서의 SSL Handshake 


  SSL_accept() API는 SSL 클라이언트로부터 시작되는 SSL handshake를 위해 대기한다. 이 API가 성공적으로 완료되었다면, SSL handshake 역시 무사히 완료되었음을 나타낸다.

1
err = SSL_accept(ssl);
cs



SSL 클라이언트에서의 SSL Handshake


  SSL 클라이언트는 SSL handshake를 시작하기 위해서 SSL_connect() API를 호출한다. API가 1을 리턴한 경우, handshake는 성공적으로 완료된 것이다. 이 연결 이후부터 데이터는 안전하게 전송될 수 있다.

1
 err = SSL_connect(ssl);
cs



SSL_read와 SSL_write를 사용한 SSL HAndshake 수행(선택 사항)


  선택 사항으로 SSL handshake를 성공시기고, SSL 데이터 교환을 수행 가능한 SSL_write()와 SSL_read()를 호출할 수 있다. 이 방법을 위해서는 SSL 서버에서 SSL_read()를 호출하기 전에 SSL_set_accept_state()를 호출해야 한다. 또한 클라이언트에서는 SSL_write()를 호출하기 전에 SSL_set_connect_state()를 호출해야 한다. 예를 들면:

1
2
3
4
5
6
7
 /* SSL_accept()가 호출되지 않은 경우, SSL_set_accept_state() */ 
 /* SSL_read()를 위해 이전 호출되어야 함 */
 SSL_set_accept_state(ssl);
 
 /* SSL_connect()가 호출되지 않은 경우, SSL_set_connect_state() */ 
 /* SSL_write()를 위해 이전에 호출되어야 함 */
 SSL_set_connect_state(ssl);
cs



Peer의 인증서 획득 (선택 사항)


  선택 사항으로, SSL handshake 후에, SSL_get_peer_certificate()를 호출함으로써 peer의 인증서를 획득할 수 있다. 이 API는 대게 인증서 정보(예를 들어, common nane과 유효 기간)를 확인하는 것처럼 바로 인증서 검증을 하기 위해 사용된다.

1
 peer_cert = SSL_get_peer_certificate(ssl);
cs



SSL 데이터 전송


  SSL handshake를 완료한 후에, 성립된 SSL 연결을 통해 데이터는 안전하게 전송될 수 있다. SSL_write()와 SSL_read()는 SSL 데이터 전송을 위해 일반적인 TCP/IP 연결을 위해 write()와 read() 또는 send()와 recv()가 사용되는 것처럼 사용된다.



데이터 전송


  SSL 연결을 통해 데이터를 전송하기 위해서는 SSL_write()를 호출한다. 데이터는 두 번째 인자로써 지정된 버퍼에 저장되고 보내진다. 예를 들면:

1
 err = SSL_write(ssl, wbuf, strlen(wbuf));
cs



데이터 수신


  SSL 연결을 통해 peer로부터 전송된 데이터를 수신하기 위해서는 SSL_read()를 호출한다. 수신된 데이터는 두 번째 인자로써 지정된 버퍼에 저장된다. 예를 들면:

1
 err = SSL_read(ssl, rbuf, sizeof(rbuf)-1);
cs



SSL 데이터 전송을 위해 BIO 사용(선택 사항)


  SSL_write()와 SSL_read()를 사용하는 대신, buffer BIO가 생성되고 세팅된 경우, 다음과 같이 BIO_puts()와 BIO_gets(), BIO_write()와 BIO_read()를 호출함으로써 데이터를 전송할 수 있다:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 BIO     *buf_io, *ssl_bio;
 char    rbuf[READBUF_SIZE];
 char    wbuf[WRITEBUF_SIZE]
 
 buf_io = BIO_new(BIO_f_buffer());        /* buffer BIO 생성 */
 ssl_bio = BIO_new(BIO_f_ssl());        /* ssl BIO 생성 */
 BIO_set_ssl(ssl_bio, ssl, BIO_CLOSE);  /* SSL에 ssl BIO를 배치 */
 BIO_push(buf_io, ssl_bio);             /* buf_io에 ssl_bio를 추가 */
 
 ret = BIO_puts(buf_io, wbuf);         
 /* buf_io 내부에 wbuf[]의 내용을 작성 */
 ret = BIO_write(buf_io, wbuf, wlen);
 /* buf_io 내부에 wbuf[]의 내용을 wlen-byte로 작성 */
 
 ret = BIO_gets(buf_io, rbuf, READBUF_SIZE);  
 /* buf_io로부터 데이터를 읽고, rbuf[]에 저장 */
 ret = BIO_read(buf_io, rbuf, rlen);
 /* buf_io로부터 rlen-byte로 데이터를 읽고, rbuf[]에 저장 */
cs



SSL 연결 종료


  SSL 연결을 종료하는 경우, SSL 클라이언트와 서버는 각각 SSL 종료를 알리기 위해 close_notify 메시지를 보낸다. peer에게 close_notify 경고를 보내기 위해서는 SSL_shutdown() API를 사용하면 된다.


  종료에 대한 절차는 두 단계로 구성된다:

  • close_notify 종료 경고 전송
  • peer로부터 close_notify 종료 경고 수신

  SSL 연결을 종료하기 위해서 다음과 같은 룰이 적용된다:

  • 각각의 파티는 close_norify 경고를 전송함으로써 종료를 시작 가능
  • 종료 경고를 보낸 후에 수신된 모든 데이터는 무시됨
  • 각각의 파티는 연결의 전송 부분을 종료하기 전에 close_notify 경고를 보낼 필요가 있음
  • 종료 개시자는 연결의 수신 부분이 종료하기 전에 close_notify 경고를 응답하기 위해 대기할 필요가 없음

  SSL 종류를 개시하는 SSL 클라이언트나 서버는 한 쪽이나 양 쪽에서 SSL_shutdown()을 호출한다. API를 양쪽에서 호출한 경우, 한 쪽의 호출은 close_notify 경고를 보내고 또 한 쪽의 호출은 peer로부터 응답을 받는다. 오로지 한 쪽의 개시자만 API를 호출한 경우는, 개시자는 peer로부터 close_notify 경고를 받지 않는다. (개시자는 응답 경고를 위해 기다릴 필요가 없기 때문에)


  경고를 수신한 peer는 개시자 쪽에 경고를 전송하기 위해 SSL_shutdown()를 한 번 호출한다.



SSL 연결 재개


  새로운 SSL 연결을 생성하기 위해 이미 성립된 SSL 세션으로부터 정보를 재사용하는게 가능하다. 왜냐하면 새로운 SSL 연결은 같은 master secret을 재사용하기 때문에, SSL handshake는 보다 빠르게 수행될 수 있다. 그 결과로써, SSL 세션 재개는 많은 SSL 연결을 허용하는 서버의 로드를 감소시킬 수 있다. 


  SSL 클라이언트는 SSL 세션을 재개하기 위해 다음의 각 단계를 수행한다:

  1. 첫 번째 SSL 연결을 시작한다. SSL 세션이 생성된다.

    1
    2
    3
    ret = SSL_connect(ssl)
     
    /* (SSL 연결을 통해 데이터 통신을 하기 위해 SSL_read() / SSL_write() 사용) */
    cs
  2. SSL 세션 정보 저장
    1
    2
    /* sess는 SSL_SESSION 이며, ssl은 SSL 이다. */
    sess = SSL_get1_session(ssl);  
    cs
  3. 첫 번째 SSL 연결 종료
    1
    SSL_shutdown(ssl);
    cs
  4. 새로운 SSL  구조체 생성
    1
    ssl = SSL_new(ctx);
    cs
  5. SSL_connect()를 호출하기 전에 새로운 SSL 세션에 이전 연결의 SSL 세션 설정
    1
    2
    SSL_set_session(ssl, sess);
    err = SSL_connect(ssl);
    cs
  6. 세션을 재개하여 두 번째 SSL 연결 시작
    1
    2
    3
    ret = SSL_connect(ssl)
     
    /* (SSL 연결을 통해 데이터 통신을 하기 위해 SSL_read() / SSL_write() 사용) */
    cs

   SSL 클라이언트가 SSL_get1_session()과 SSL_set_session()을 호출하는 경우, SSL 서버는 세션을 재개하기 위해서 특별히 API를 호출할 필요 없이 같은 세션을 사용하여 새로운 SSL 연결을 허용 가능하다. 서버는 "SSL 구조체 생성 및 설정", "TCP/IP 연결 설정", "Socket/SSL 구조체 내부의 Socket BIO 설정", "SSL Handshake", "SSL 데이터 전송"에서 설명한 각 단계를 거치게 된다.

※ 참고 : SSL_free()를 호출하면, SSL_get1_session을 사용하여 SSL 세션을 저장하였더라도, SSL 세션의 재사용은 실패한다.



SSL Handshake 재협상


  SSL 재협상은 이미 연결된 SSL 연결을 통한 새로 수행하는 SSL handshake이다. 재협상 메시지(암호의 타입 및 암호키)는 암호화되고 이미 존재하는 SSL 연결을 통해 전송함으로써, SSL 재협상은 안전하게 또 다른 SSL 세션을 성립 시킬 수 있다. SSL 재협상은 기본적인 SSL 세션을 성립시킨다면, 다음과 같은 상황에서 유용하다.

  • 클라이언트 인증이 필요한 경우
  • 암호화와 복호화 키를 기존과 다르게 세팅하여 사용하고자 하는 경우
  • 암호화와 해시 알고리즘을 기존과 다르게 세팅하여 사용하고자 하는 경우

  SSL 재협상은 SSL 클라이언트나 SSL 서버 중 한 쪽에서 개시될 수 있다. 클라이언트에서의 SSL 재협상 개시는 서버에서 개시를 위해 필요한 API를 위해 기존과 다른 API 설정(개시한 SSL 클라이언트와 허용한 서버 양 쪽 전부)이 필요하다.


  다음의 섹션에서는 양 쪽의 상황을 위해 요구되는 API를 설명한다.

※ 참고 : SSLv2는 SSL 재협상을 수행할 수 없다. 이 작업을 수행하려면 SSLv3이나 TLSv3을 사용해야 한다.


SSL 서버를 통해 개시하는 SSL 재협상


  SSL 서버로부터 SSL 재협상을 개시하기 위해서는, 한 번의 SSL_renegotiate()와 두 번의 SSL_do_handshake()를 호출해야 한다.


  SSL_renegotiate() API는 SSL 재협상을 위해 flags를 설정한다. 이 API는 실제로는 재협상을 개시하지 않는다. SSL_renegotiate()를 통해 설정되는 flags는 SSL 클라이언트와 SSL 재협상을 수행할 필요가 있다고 SSL_do_handshake()에게 알리는 역할이다. SSL_do_handshake() API는 실제로 SSL handshake를 수행한다. 이 API를 첫 번째 호출하는 경우에는 SSL 클라이언트에게 -Server Hello- 메시지를 전송한다.


  성공적으로 첫 번째 호출이 이루어진 경우, 클라이언트는 SSL 재협상 수행에 동의하게 된다. 서버는 그런 다음에 SSL 구조체에 SSL_ST_ACCEPT 상태가 세팅되고 재협상의 나머지를 수행하기 위해서 SSL_do_handshake()를 다시 한번 호출한다.


  다음의 코드는 이 API들이 어떻게 사용되는지 보여준다:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
printf("Starting SSL renegotiation on SSL server (initiating by SSL server)");
    if(SSL_renegotiate(ssl) <= 0){
        printf("SSL_renegotiate() failed\n");
        exit(1);
    }
 
    if(SSL_do_handshake(ssl) <= 0){
        printf("SSL_do_handshake() failed\n");
        exit(1);
    }
 
    ssl->state = SSL_ST_ACCEPT;
 
    if(SSL_do_handshake(ssl) <= 0){
        printf("SSL_do_handshake() failed\n");
        exit(1);
    }
cs


  다음의 코드는 서버를 통해 재협상이 개시된 경우에 SSL 클라이언트를 통해 해당 API가 호출되는 것을 보여준다:

1
2
3
printf("Starting SSL renegotiation on SSL client (initiating by SSL server)");       
/* SSL  */
err = SSL_read(ssl, buf, sizeof(buf)-1);
cs


  예제에서 보았듯이, SSL_read()는 데이터 교환을 수행하고, 재협상같은 연결과 관련된 함수를 처리하는 것또한 가능하다.



SSL 클라이언트를 통해 개시하는 SSL 재협상


  SSL 클라이언트도 SSL 재협상을 개시할 수 있다. 여기에서, 클라이언트에서 재협상을 개시하기 위한 설정은 서버에서 재협상을 개시하기 위해서 수행하는 것과 비슷하다. 이 작업이 완료되기 위해서는, SSL 클라이언트는 SSL_renegotiate()와 SSL_do_handshake()를 오로지 한 번씩만 호출한다. SSL_renegotitate()는 SSL 재협상을 위해 그저 flag를 세팅하고, SSL_do_handshake()를 한 번 호출하면서 재협상 전체를 수행하게 된다.

1
2
3
4
5
6
7
8
9
printf("Starting SSL renegotiation on SSL client (initiating by SSL client)");
if(SSL_renegotiate(ssl) <= 0){
    printf("SSL_renegotiate() failed\n");
    exit(1);
}
if(SSL_do_handshake(ssl) <= 0){
    printf("SSL_do_handshake() failed\n");
    exit(1);
}
cs


  다음의 코드는 클라이언트를 통해 재협상이 개시된 경우에 SSL 서버를 통해 호출되는 API를 보여준다.(서버를 통해 재협상이 개시된 경우와 SSL 클라이언트를 통해 호출되는 API는 동일하다.)

1
2
3
printf("Starting SSL renegotiation on SSL server (initiating by SSL client)");
/* SSL renegotiation */
err = SSL_read(ssl, buf, sizeof(buf)-1);
cs


  이 예제에서 다시 한번, SSL_read()는 데이터 교환과 연결 재협상을 처리한다.



SSL 어플리케이션 종료


  SSL 어플리케이션 프로그램을 종료하는 경우, 주 task는 어플리케이션 프로그램에서 생성되고 사용된 데이터 구조체를 free(deallocate) 된다. 해제를 위한 API에는 일반적으로 _free 접미사가 포함되지만, 새로운 데이터 구조체를 생성하는 API에는 _new 접미사가 포함된다.


  SSL 어플리케이션 프로그램에서 생성되었던 데이터 구조체들은 해제해야 한다. xxx_new() API를 사용하여 또 다른 구조체 내부에 생성된 데이터 구조체는 구조체가 해당 구조체에 적합한 xxx_free() API를 사용하여 해제하는 경우에 자동적으로 해제된다. 예를 들어, SSL_new()를 사용하여 생성한 BIO 구조체는 SSL_free()를 호출한 경우에 해제된다. SSL 구조체 내부에 있는 BIO를 해제하기 위해 BIO_free()를 호출할 필요가 없다. 그러나 어플리케이션 프로그램이 BIO 구조체를 할당하기 위해 BIO_new()를 호출한 경우에는, BIO_free()를 사용하여 해당 구조체를 해제해야 한다.


※ 참고 : SSL_free()를 호출하기 전에, SSL_shutdown()을 호출해줘야 한다.