Dev

non-blocking socket에 OpenSSL 적용하기

prostars 2016. 12. 15. 10:53

정말 오랜만에 포스팅한다.


이 문서에서 사용하는 OpenSSL 버전은 1.0.1u 이다.

하트블리드 취약점을 피하려면 1.0.1g  이상을 사용해야 한다.

공식 OpenSSL 페이지 : https://www.openssl.org/

이 문서에서 OpenSSL의 빌드, 프로젝트에 설정하는 방법 그리고 자세한 에러 처리는 생략한다.
이제 개발에 필요한 내용만 정리해보자.

아래 내용이면 바로 non-blocking socket에 SSL을 적용할 수 있을 것이다.

이 문서에서 필요한 헤더 파일은 아래와 같다.

ssh.h
bio.h
err.h
engine.h
conf.h


사설 인증서 생성

테스트를 위해서 인증서가 필요하므로 사설 인증서를 하나 만들자.
아래와 같이 실행하면 사설 인증서를 만들 수 있다.


openssl req -x509 -nodes -days 365 -newkey rsa:1024 -keyout mycert.pem -out mycert.pem


OpenSSL 초기화 / 해제

우선 OpenSSL을 사용하기 위해서 아래와 같이 초기화를 한다.


SSL_library_init();

SSL_load_error_strings();

ERR_load_BIO_strings();


그리고 SSL을 사용하기 위한 context 객체가 필요하다.

아래와 같이 사용할 SSL 버전을 선택하고 SSL Context를 생성한다.

여기서는 TLS 1.2 버전을 지정했다.


SSLCtx = SSL_CTX_new(TLSv1_2_server_method());


이제 위에서 생성한 사설 인증서를 로딩하도록 아래와 같이 설정하고 SSL_CTX_check_private_key으로 검증한다.


SSL_CTX_use_certificate_file(SSLCtx, "mycert.pem", SSL_FILETYPE_PEM);

SSL_CTX_use_PrivateKey_file(SSLCtx, "mycert.pem", SSL_FILETYPE_PEM);

SSL_CTX_check_private_key(SSLCtx);


SSL_CTX_check_private_key이 에러를 리턴한다면 인증서에 문제가 있는 것이다.


SSL의 사용이 끝나면 SSL_CTX_free을 사용해서 정리하고, 아래와 같이 사용한 모든 자원을 해제한다.


SSL_CTX_free(SSLCtx);


ERR_remove_state(0);

ENGINE_cleanup();

CONF_modules_unload(1);

ERR_free_strings();

EVP_cleanup();

sk_SSL_COMP_free(SSL_COMP_get_compression_methods());

CRYPTO_cleanup_all_ex_data();


SSL Accept 처리

SSL Accept 처리는 TCP Accept가 처리된 이후에 진행한다.

SSL 커넥션을 관리에 필요한 SSL 구조체를 SSL_new()를 통해서 생성하고 SSL 핸드쉐이크 처리를 SSL_accept()에 위임하기 위해서 TCP socket handle과 연결해야 한다.

연결되는 TCP socket handle은 이미 TCP accept가 완료된 socket의 handle이어야 하며 SSL_accept() 호출 전에 socket을 blocking mode로 설정하고 SSL_accept() 완료 후에 다시 non-blocking mode로 돌리는 것이 편할 것이다.


SSL* ssl = SSL_new(SSLCtx);    // SSLCtx는 위에서 SSL_CTX_new()로 생성한 것이다.

SSL_set_fd(ssl, static_cast<int>(socketHandle));

SSL_accept(ssl);


SSL non-Blocking 통신

SSL Accept 처리가 완료된 후에 간단히 SSL_read(), SSL_write()를 사용하여 암호화 통신을 사용할 수는 있지만 기본적으로 blocking mode로 동작한다.
Memory BIO를 사용하여 SSL 처리를 TCP socket 통신처리와 분리하면 손쉽게 기존의 non-blocking socket에 SSL를 적용할 수 있다.
Memory BIO를 사용하려면 아래와 같이 read, write를 위한 BIO를 생성해야 하고, 위에서 SSL_new()로 생성한 SSL과 연결해야 한다.

BIO* rbio = BIO_new(BIO_s_mem());
BIO* wbio = BIO_new(BIO_s_mem());
SSL_set_bio(ssl, rbio, wbio);

이제 SSL_accept() 처리를 위해서 연결했던 socket handle과 분리되어 socket과의 종속성이 사라졌다.
기존 socket 통신 로직은 그대로 사용하면서 send / recv 처리 앞에 암복호화 처리만 추가해주면 된다.

암호화 처리를 보면 아래와 같다.

// ssl은 위에서 SSL_new로 생성한 것이다.
// SSL_write는 source 메모리의 내용을 암호화하여 연결한 BIO 메모리에 기록한다.
int writtenLength = SSL_write(ssl, sourceMemoryPointer, lengthForWrite);

int bioWrittenLength = BIO_number_written(wbio);    // 암호화된 내용의 길이를 가져온다.

// 암호화된 내용을 target 메모리에 복사한다.
int len = BIO_read(wbio, targetMemoryPointer, bioWrittenLength);

암호화 처리는 어떤 내용을 네트워크로 내보내기 전에 한 번에 암호화하여 처리하는 것이 편하다.
복호화 처리는 이와는 조금 다른데 이는 보내는 쪽에서 1000byte를 보냈다고 받는 쪽이 1000byte를 한번에 받는다는 보장이 없기 때문이다.

복호화 처리는 아래와 같다.

// ssl은 위에서 SSL_new로 생성한 것이다.
// BIO_write는 source 메모리의 내용을 복호화하여 연결한 SSL에서 읽을 수 있도록 한다.
int writtenLength = BIO_write(rbio, sourceMemoryPointer, lengthForWrite);

int bioWrittenLength = BIO_number_written(wbio);    // 복호화된 내용의 길이를 가져온다.

/* 
복호화된 내용을 target 메모리에 복사한다.
SSL_read()가 리턴한 값이 0보다 작으면 SSL_get_error(ssl, len); 를 호출하여 error code가 SSL_ERROR_WANT_READ인지 확인하고 SSL_ERROR_WANT_READ라면 아직 암호화된 내용이 모두 도착하지 않아 복호화에 실패한 것이다. 
*/
int len = SSL_read(ssl, targetMemoryPointer, bioWrittenLength);    


SSL_ERROR_WANT_READ에 대한 처리는 추가로 들어오는 패킷을 받아서 BIO_write()에 계속 써주면 된다.

간단히 말해서 SSL_ERROR_WANT_READ가 발생하면 위의 처리를 그대로 다시 해주면 된다.

암호화된 내용이 모두 모이지 않으면 복호화할 수 없어서 발생하는 것으로 당연한 처리이지 예외 사항이 아니다.


SSL Close 처리

socket close 처리를 할 때 해당 socket을 위해서 생성했던 SSL로 같이 정리해야 한다.
Memory BIO를 사용했으므로 SSL Accept 처리 이후에는 socket handle과 종속관계가 사라지므로 socket handle의 close 순서와는 관련이 없다.

아래와 같이 정리하면 된다.


SSL_shutdown(ssl);

SSL_free(ssl);    // 이때 연결된 BIO도 모두 해제된다.


간단한 SSL 인증과 송수신 Test

OpenSSL을 설치하면 같이 포함되어 있는 openssl command line tool을 사용하여 간단한 테스트를 진행할 수 있다.

위의 내용을 구현한 SSL server를 실행하고 아래와 같이 openssl command line tool을 실행한다. (ssl port는 443이라고 가정한다.)


openssl s_client -connect 127.0.0.1:443 -debug -tls1_2


위에 사용한 아규먼트들은 ssl client 모드로 로컬 호스트의 443포트에 TLS 1.2 프로토콜을 사용하여 debug 모드로 접속하겠다는 것이다.

debug 모드는 추가 정보들을 보여준다.

실행하고 접속에 성공하면 SSL Handshake 과정과 인증서 정보가 출력되며 사용자 입력을 기다린다.

이때 동작은 간단한 채팅 클라이언트 처럼 동작한다. 콘솔에 입력된 내용은 엔터를 치면 서버로 전송되고 서버로 부터 받은 내용은 콘솔에 그대로 출력된다.

간단한 ssl echo server를 구현하여 테스트하기에 편리하다.


이상으로 정리를 마칩니다.

위 내용에서 잘못된 내용은 꼭 알려주시기를 부탁드립니다.


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

이 글이 TOAST Meetup에 소개되었습니다.

non-blocking socket에 OpenSSL 적용하기


반응형