도커 debian:trixie 컨테이너환경에서 작성된 글입니다.

ai의 도움과 여러 블로그를 참조하며 작성된 게시글입니다.


 

SSL/TLS 그리고 인증서란 무엇인가

유튜브 채널 생활코딩의 강의영상을 보고 정리한 글입니다.SSL/TLShttp와 https인터넷을 하면 사이트 주소 앞에 항상 https가 붙어있다. http는 html 문서를 전송하기 위해 만들어진 통신규약이고, https

dodontak.tistory.com

지난 글에서 SSL과 TLS에 대해 공부했다.

이번엔 실제로 C/C++ 코드로 openssl 라이브러리를 사용하여 TLS를 TCP소켓 통신에 적용시켜보자.

 

개인키 생성, 인증서 생성(자체 서명)

그전에 먼저 TLS통신을 위한 개인키와 인증서를 만들자. 공용키는 인증서에 포함되어있다.

openssl genpkey -algorithm RSA -out server.key -pkeyopt rsa_keygen_bits:2048
openssl req -new -x509 -key server.key -out server.crt -days 365

rsa 키는 수학적으로 개인키, 공개키 한쌍으로 구성된다. 즉 개인키 안에 공개키를 계산할 수 있는 정보가 이미 들어있는 것.

인증서의 Issuer(발급자)와 Subject(주체)가 같기때문에 self-signed 인증서다. 발급자가 클라이언트의 신뢰 CA 리스트에 없기 때문에 신뢰할 수 없는 인증서지만, TLS 악수는 정상 완료된다. 하지만 클라이언트 정책에 따라 연결이 끊길 수 있다.

아래 접은글에 genpkey와 req가 무슨 명령어인지, 각 옵션이 무슨 뜻인지 정리했다.

더보기
더보기

genpkey : 개인키 생성 or 키 쌍 생성

openssl genpkey [-out filename] [-algorithm alg] [-pkeyopt opt:value]

  • -out filename : 개인키 이름 설정
  • -algorithm alg : 사용할 공개 키 알고리즘. 유효한 내장 알고리즘 이름은 RSA, RSA-PSS, EC, X25519, X448, ED25519 및 ED448
  • -pkeyopt opt:value : 공개키 알고리즘 옵션. 공개키 알고리즘에 따라 다름.

openssl 문서를 보면 더 많은 옵션이 제공된다.

https://docs.openssl.org/3.4/man1/openssl-genpkey/#options

 

openssl-genpkey - OpenSSL Documentation

openssl-genpkeyNAMEopenssl-genpkey - generate a private key or key pairSYNOPSISopenssl genpkey [-help] [-out filename] [-outpubkey filename] [-outform DER|PEM] [-verbose] [-quiet] [-pass arg] [-cipher] [-paramfile file] [-algorithm alg] [-pkeyopt opt:value

docs.openssl.org

더보기
더보기

req : 인증서 요청 및 인증서 생성 명령

openssl req [-new] [-x509] [-key filename | uri] [-out filename] [-days n]

  • -new : 새 인증서 생성
  • -x509 : 인증서 요청 대신 출력 -CA 옵션에 포함됨. -in 이 주어지지 않은 경우 -new 플래그 의미.
  • -key filename | uri : 새 인증서 또는 인증서 요청에 서명하는데 사용할 개인키 제공 -in 옵션 없으면 해당 공개 키가 새 인증서 또는 인증서 요청에 포함되어 자체 서명이 생성됨.
  • -out filename : 출력파일 이름
  • -days n : -x509옵션 사용할 경우 인증서의 유효기간 설정. 디폴트값 30

openssl 문서를 보면 훨씬 더 많은 옵션이 제공된다.

https://docs.openssl.org/3.6/man1/openssl-req/#options

 

openssl-req - OpenSSL Documentation

openssl-reqNAMEopenssl-req - PKCS#10 certificate request and certificate generating commandSYNOPSISopenssl req [-help] [-cipher] [-inform DER|PEM] [-outform DER|PEM] [-in filename] [-passin arg] [-out filename] [-passout arg] [-text] [-pubkey] [-noout] [-v

docs.openssl.org


openssl 라이브러리

C/C++ 코드로 TLS를 사용하기 위해 openssl 라이브러리를 설치한다.

apt install -y libssl-dev

 

컴파일러 라이브러리 링크 옵션

-lssl -lcrypto

 

openssl 라이브러리 구조체

코드 짜면서 접한 구조체만 적었지만 이외에도 여러 구조체가 있다.

그리고 이 글에서는 기본소켓에 SSL만 덮어서 쓰는데, BIO 라는 객체를 쓰는 더 high level 한 방법도 있다고 한다.

SSL_CTX 구조체

TLS정책과 설정을 담는 SSL 공장

  • 인증서, 개인키, 암호 스위트, 옵션, 세션 캐시등 변경되지 않을 TLS 서버 전체 설정을 담는 상위 객체.
  • 이후 생성되는 SSL 객체의 기본 설정값 제공하는 템플릿 역할.
  • 서버를 시작할 때 1번 생성하고 종료할 때 1번 해제하면 된다.
  • 읽을 일만 있게 설계되었기 때문에 읽기 전용 처럼 사용된다.
  • openssl 1.1.0 이후로 내부적으로 쓰레드 세이프하게 동작한다.

SSL구조체

개별 연결의 암호화 상태 머신

  • 현재 핸드셰이크 상태, 세션 키, 읽기/쓰기 암호 상태,  레코드 버퍼 등 암호화 통신의 실시간 상태 담는 객체.
  • 암호화 통신을 하는 클라이언트마다 하나씩 만들어 담당한다.
  • 연결시 생성하고 연결 종료 시 해제한다.
  • 연결별 상태를 가지므로 쓰레드간 공유하면 안된다.

SSL서버와 SSL클라이언트 통신방법

구조체들 생성 파괴함수

openssl 라이브러리 함수들

코드 짜면서 접한 함수들만 나열했지만 이 외에도 어마어마하게 많다.

CTX 객체 설정

  • SSL_METHOD* TLS_server_method() : TLS서버로 동작하곘다. 라는 설정 구조체 리턴.
  • SSL_CTX* SSL_CTX_new(method) : TLS설정 컨텍스트 생성. 인증서, 암호 스위트, 옵션 등을 담는 공장. 서버 전체 설정
  • SSL_CTX_use_certificate_file(ctx, "cert.pem", SSL_FILETYPE_PEM) : 서버에서 쓸 인증서 설정
  • SSL_CTX_use_PrivateKey_file(ctx, "key.pem", SSL_FILETYPE_PEM) : 서버에서 쓸 개인키 설정

SSL 객체 설정

  • SSL* SSL_new(ctx) : 새 TLS객체 생성
  • int SSL_set_fd(ssl, client_fd) : TLS객체와 TCP소켓 연결. 이제 SSL_read, SSL_write는 내부에서 알아서 암호화/복호화 한다.
  • int SSL_accept(ssl) : TLS 핸드셰이크 수행. accept로 클라이언트와 연결된 후에 사용한다.

I/O 함수

TLS는 사용자 입장에서 여전히 스트림처럼 동작하기 때문에 10글자의 평문을 보냈다고 그게 꼭 10글자를 암호화해서 보낼것이라는 보장은 없다. 평문이 3 4 3 이렇게 오갈 수 있는 것.

  • int SSL_read(ssl, buffer, sizeof(buffer)) : TLS 레코드 수신. read 대신 쓴다.
    실제로 읽은 바이트를 리턴한다. 리턴된 값은 평문의 길이이다. 만약 암호문이 해석할 수 있을만큼 길게 오지 않았다면 읽지 않고 대기하거나 nonblock이라면 에러코드로 실패 이유를 확인해야한다. 
  • int SSL_write(ssl, buffer, bytes) : 평문을 암호화 후 전송. write 대신 쓴다.
    실제로 쓴 바이트를 리턴한다.

통신 중단 함수

  • SSL_shutdown(ssl) : TLS종료 절차 수행. 이것을 마친 후에 close(fd) 해야한다.
  • SSL_free(ssl) : SSL 객체 메모리 해제.
  • SSL_CTX_free(ctx) : 서버 전체 TLS 설정 해제. CTX 객체 메모리 해제. 서버 종료할 때 호출한다.

기타

  • ERR_print_errors_fp(stderr) : openssl 내부 에러스택에 쌓인 에러를 fd에 출력하는 코드. 더 현대화된 방법도 있다고 함.
  • ERR_get_error() : 에러코드를 리턴. 에러를 커스터마이징 할 수 있다.
  • const char* SSL_get_version(const SSL *ssl) : TLS 버전 확인

더이상 사용되지 않는 함수

  • SSL_load_error_strings() : 에러 문자열 테이블 로딩. 지금은 자동처리된다.
  • OpenSSL_add_ssl_algorithms() : 암호 알고리즘 등록. 지금은 자동 등록된다.
  • EVP_cleanup() : 알고리즘 정리. 지금은 내부적으로 자동 관리된다.

소스코드

채팅서버에 SSL을 적용시켜보자.

코드는 지난 I/O multiplexing 글에서 만든 epoll코드를 개조했다.

listen 소켓 만드는 과정, epoll 세팅 과정은 이번 주제가 아니라서 최대한 함수로 빼고 main문을 간소화했다.

코드가 너무길어서 전방선언된 함수들은 더보기로 빼놨으니 확인.

 

클라이언트 소켓을 논블록으로 하니 SSL_accept에서 문제가 생겨서 블로킹으로 바꿨다.

논블록으로 하고도 문제가 안생기려면 에러옵션 처리를 잘 하면 되는데 주제가 암호화니 대충 넘어갔다.

#include <iostream>
#include <vector>
#include <map>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <string>
#include <sys/epoll.h>
#include <stdlib.h>

#include <openssl/ssl.h>
#include <openssl/err.h>

using namespace std;

void	handle_error(const char* err_str, int rtn);
int		epoll_setting(int listen_sock);
int		open_listen_socket(int port);
void	make_socket_nonblock(int sock);
void	broadcast(int fd, const map<int, SSL*>& ssls, const char* buffer, int size);

SSL_CTX* create_context()
{
    const SSL_METHOD* method = TLS_server_method();
    SSL_CTX* ctx = SSL_CTX_new(method);
    if (!ctx)
		handle_error("SSL_CTX_new error", 1);
    return ctx;
}

void configure_context(SSL_CTX* ctx)
{
    if (SSL_CTX_use_certificate_file(ctx, "server.crt", SSL_FILETYPE_PEM) <= 0)
    	handle_error("SSL_CTX_use_certificate_file error", 1);

    if (SSL_CTX_use_PrivateKey_file(ctx, "server.key", SSL_FILETYPE_PEM) <= 0)
    	handle_error("SSL_CTX_use_PrivateKey_file error", 1);
}

int main(int ac, char** av)
{
	if (ac != 2)
		handle_error((string(av[0]) + " [port]").c_str(), 1);
//        ##### CTX 생성, 설정 #####
	SSL_CTX* ctx = create_context();
	configure_context(ctx);

//        ##### listen socket 생성, 설정 #####
	int listen_sock = open_listen_socket(atoi(av[1]));
//        ##### epoll 생성, 설정 #####
	int epollfd = epoll_setting(listen_sock);
	vector<struct epoll_event>	events(1000);//최대 감시 확인 1000개
	map<int, SSL*>				ssls;//(fd, SSL) 페어 저장
//        ##### 채팅 서버 시작 #####
	while (true)
	{
		int nfds = epoll_wait(epollfd, events.data(), events.size(), -1);
		for (int n = 0; n < nfds; n++)
		{
			int fd = events[n].data.fd;

			if (fd == listen_sock)
			{// 새 클라이언트 connect
				struct sockaddr_in client_addr;
				socklen_t sock_len = sizeof(struct sockaddr_in);
				int client_sock = accept(listen_sock,
                	(struct sockaddr*)&client_addr, &sock_len);
				if (client_sock == -1) {
					if (errno == EAGAIN)
						break;
					handle_error("accept error", 1);
				}
				SSL* ssl = SSL_new(ctx);
				SSL_set_fd(ssl, client_sock);
				if (SSL_accept(ssl) <= 0)
					handle_error("SSL_accept error", 1);
				ssls.insert({client_sock, ssl});
				struct epoll_event ev;
				ev.events = EPOLLIN;
				ev.data.fd = client_sock;
				if (epoll_ctl(epollfd, EPOLL_CTL_ADD, client_sock, &ev) == -1)
					handle_error("epoll_ctl client add error", 1);
			} else
			{ // 클라이언트가 서버로 메세지 보냄
				char read_buffer[1000];
				int rd = SSL_read(ssls[fd], read_buffer, 1000);
				if (rd <= 0)
				{
					int err = SSL_get_error(ssls[fd], rd);
					if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) {
						continue;
					} else if (err == SSL_ERROR_ZERO_RETURN ||
                    		(err = SSL_ERROR_SYSCALL && rd == 0)) {
						// 클라이언트가 나감
						epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, NULL);
						SSL_shutdown(ssls[fd]);
						SSL_free(ssls[fd]);
						ssls.erase(fd);
						close(fd);
						broadcast(fd, ssls, "out\n", 4);
						continue;
					} else {
						handle_error("SSL_read real error", 1);
					}
				}
				broadcast(events[n].data.fd, ssls, read_buffer, rd);
			}
		}
	}
	SSL_CTX_free(ctx);
}
더보기
더보기
void handle_error(const char* err_str, int rtn)
{
	cerr << err_str << endl;
	exit(rtn);
}

int epoll_setting(int listen_sock)
{
	struct epoll_event ev;
	ev.data.fd = listen_sock;
	ev.events = EPOLLIN;
	int epollfd = epoll_create1(0);
	if (epollfd == -1)
		handle_error("epoll_create1 error", 1);
	if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1)
		handle_error("epoll_ctl listen_sock error", 1);
	return epollfd;
}

int open_listen_socket(int port)
{
	int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
	if (listen_sock == -1)
		handle_error("socket error", 1);
	make_socket_nonblock(listen_sock);
	struct sockaddr_in sock_addr;
	memset(&sock_addr, 0, sizeof(sock_addr));
	sock_addr.sin_family = AF_INET;
	sock_addr.sin_port = htons(port);
	sock_addr.sin_addr.s_addr = INADDR_ANY;

	if (bind(listen_sock, (struct sockaddr *)&sock_addr, sizeof(sock_addr)) == -1)
		handle_error("bind error", 1);
	if (listen(listen_sock, 10) == -1)
		handle_error("listen error", 1);
	return listen_sock;
}

void make_socket_nonblock(int sock)
{
	int flags = fcntl(sock, F_GETFL, 0);
	if (flags == -1)
		handle_error("fcntl F_GETFL error", 1);
	if (fcntl(sock, F_SETFL, flags | O_NONBLOCK) == -1)
		handle_error("fcntl NONBLOCK error", 1);
}

void broadcast(int fd, const map<int, SSL*>& ssls, const char* buffer, int size)
{
	string str;
	str = "client " + to_string(fd) + ": " + string(buffer, size);
	for (map<int, SSL*>::const_iterator it = ssls.begin(); it != ssls.end(); it++)
		SSL_write(it->second, str.c_str(), str.length());
}

 

터미널에서 openssl 사용해 SSL서버에 접속하기

클라이언트로 접속하는 명령어 주소와 포트는 서버에 맞춰서 적는다.

openssl s_client -connect localhost:4242

여러개의 클라이언트로 접속해봤는데 입장, 퇴장, 채팅 모두 잘 작동한다.

 

일반 read로 읽었을 때와 SSL_read로 읽었을 때의 차이.

서버와 통신하는 클라이언트를 만들어 테스트해봤다. (클라는 gpt로 대충 만들고 SSL만 씌움)

좌 2개 일반read, 우 1개 SSL_read

일반  read로도 읽을 수는 있지만 복호화 되지 않아서 알아볼 수가 없다. 그리고 같은 문자열도 매번 암호화 된 모양이 다르다.


참고한 블로그

 

OpenSSL 이해하기

[OPENSSL-1.0.2] 코드로 알아보는 SSL/TLS 통신이해 1. SSL 통신을 위해 필요한 구조체 1)...

blog.naver.com

 

글작성에 참고하지는 않았지만 도움이 될 것 같은 블로그

 

OpenSSL을 이용한 SSL 인증서 발급 방법

웹서버에 보안을 위하여 SSL 인증서를 발급받아 설치할 수 있습니다. SSL 인증서의 경우, 공인된 업체를 통하여 일정 금액을 지불하면 발급 받을 수 있습니다. 하지만 개인적으로 사용하거나 개발

daehancni.tistory.com

 

'공부 > CS' 카테고리의 다른 글

SIGPIPE  (0) 2026.03.02
openssl SSL_get_error 에러코드 with.C/C++  (0) 2026.02.26
스레드 풀 thread pool  (0) 2026.02.24
SSL/TLS 그리고 인증서란 무엇인가  (0) 2026.02.23
I/O Multiplexing epoll 채팅서버 만들기 with.C/C++  (0) 2026.02.21

+ Recent posts