C언어로 SSL/TLS를 활용한 안전한 통신 구현 방법

C언어를 활용해 네트워크 상에서 안전한 통신을 구현하려면 SSL/TLS를 이해하고 적용하는 것이 필수적입니다. SSL/TLS는 데이터 암호화와 인증을 통해 해커로부터 데이터를 보호하며, 오늘날 대부분의 보안 프로토콜의 기본이 됩니다. 본 기사에서는 SSL/TLS의 기본 개념부터 C언어로 안전한 통신을 구현하는 구체적인 방법까지 실용적인 정보를 제공합니다. 이를 통해 보안성이 뛰어난 네트워크 애플리케이션을 개발할 수 있는 기반을 마련할 수 있습니다.

목차

SSL/TLS란 무엇인가?


SSL(보안 소켓 계층)과 TLS(전송 계층 보안)는 인터넷 상에서 데이터를 안전하게 전송하기 위한 보안 프로토콜입니다.

SSL과 TLS의 차이점


SSL은 초기 보안 프로토콜로, 현재는 구식으로 간주됩니다. TLS는 SSL의 개선 버전으로 더 높은 보안성과 효율성을 제공합니다. 최신 환경에서는 TLS를 사용하는 것이 권장됩니다.

SSL/TLS의 주요 기능

  • 데이터 암호화: 전송 중 데이터가 노출되지 않도록 암호화합니다.
  • 무결성 확인: 데이터가 전송 중에 변조되지 않았음을 보장합니다.
  • 인증: 통신 당사자 간의 신뢰를 검증합니다.

SSL/TLS의 동작 원리


SSL/TLS는 클라이언트와 서버 간의 핸드셰이크 과정을 통해 암호화 키를 교환하고, 이후 해당 키를 사용해 데이터를 암호화하여 전송합니다. 이 과정에서 공개 키 암호화 및 대칭 키 암호화가 조합되어 사용됩니다.

SSL/TLS는 안전한 인터넷 통신의 기본이며, C언어에서 이를 활용하면 네트워크 애플리케이션의 보안성을 크게 높일 수 있습니다.

C언어에서 SSL/TLS 사용 준비

필요한 라이브러리


C언어에서 SSL/TLS를 구현하려면 OpenSSL과 같은 SSL/TLS 라이브러리가 필요합니다. OpenSSL은 SSL/TLS 통신 구현을 위한 다양한 API를 제공하며, 대부분의 플랫폼에서 지원됩니다.

개발 환경 설정

  1. 라이브러리 설치:
  • Linux: apt-get install libssl-dev (Debian/Ubuntu 기반)
  • macOS: brew install openssl
  • Windows: OpenSSL 공식 웹사이트에서 Windows용 바이너리를 다운로드 및 설치
  1. 컴파일러 설정:
    OpenSSL 라이브러리를 사용하려면 컴파일러에 헤더 파일과 라이브러리 경로를 설정해야 합니다.
   gcc -o program program.c -lssl -lcrypto

헤더 파일 포함


SSL/TLS 기능을 사용하려면 다음 헤더 파일을 포함해야 합니다.

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

SSL/TLS 초기화 코드


SSL/TLS를 사용하기 위해 프로그램의 초기화 단계에서 다음 단계를 수행해야 합니다.

SSL_library_init();        // SSL 라이브러리 초기화  
SSL_load_error_strings();  // 오류 문자열 로드  
OpenSSL_add_all_algorithms(); // 암호화 알고리즘 로드  

환경 테스트


라이브러리가 정상적으로 설치되었는지 확인하려면 간단한 프로그램을 작성하여 SSL/TLS 초기화가 성공적으로 수행되는지 확인합니다.

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

int main() {
    SSL_library_init();
    printf("SSL/TLS 라이브러리 초기화 성공\n");
    return 0;
}

C언어에서 SSL/TLS를 구현하기 위해 위 준비 단계를 완료하면 본격적인 클라이언트-서버 통신 구현에 착수할 수 있습니다.

OpenSSL 라이브러리 설치 및 설정

OpenSSL 설치


SSL/TLS 통신 구현을 위해 OpenSSL 라이브러리를 설치해야 합니다. 플랫폼별 설치 방법은 다음과 같습니다.

  1. Linux:
  • Debian/Ubuntu:
    bash sudo apt-get update sudo apt-get install libssl-dev
  • Red Hat/CentOS:
    bash sudo yum install openssl-devel
  1. macOS:
    Homebrew를 사용해 설치:
   brew install openssl
  1. Windows:
  • OpenSSL 공식 웹사이트(https://www.openssl.org)에서 Windows용 바이너리를 다운로드합니다.
  • 설치 후 libinclude 디렉터리 경로를 C 컴파일러 설정에 추가합니다.

OpenSSL 초기화


OpenSSL을 사용하려면 초기화가 필요합니다. 기본 설정은 아래 코드를 참고하세요.

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

void initialize_openssl() {
    SSL_library_init();
    SSL_load_error_strings();
    OpenSSL_add_all_algorithms();
}

void cleanup_openssl() {
    EVP_cleanup();
}

컴파일러 설정


OpenSSL을 사용하는 프로그램을 컴파일하려면 라이브러리 경로와 링크 옵션을 추가해야 합니다.
예시:

gcc -o program program.c -lssl -lcrypto

테스트 코드


OpenSSL 설치와 설정이 제대로 되었는지 확인하려면 간단한 테스트 프로그램을 작성합니다.

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

int main() {
    SSL_library_init();
    SSL_load_error_strings();
    OpenSSL_add_all_algorithms();

    printf("OpenSSL 라이브러리가 성공적으로 초기화되었습니다.\n");

    return 0;
}

환경 변수 설정 (Windows 사용 시)


Windows에서는 OpenSSL 설치 디렉터리를 환경 변수로 추가해야 할 수 있습니다.

  • PATH 변수에 OpenSSL 설치 디렉터리의 bin 경로를 추가합니다.
  • INCLUDELIB 변수에 각각 OpenSSL의 includelib 경로를 추가합니다.

라이브러리 설정 문제 해결

  • 컴파일 에러 발생 시: 라이브러리 경로나 파일 이름을 확인하고, 적절히 수정합니다.
  • 링크 에러 발생 시: -lssl -lcrypto 옵션이 누락되지 않았는지 확인합니다.

OpenSSL 설치 및 설정 과정을 완료하면 SSL/TLS를 활용한 네트워크 통신 구현을 시작할 준비가 완료됩니다.

SSL/TLS 인증서 생성 및 관리

SSL/TLS 인증서란?


SSL/TLS 인증서는 클라이언트와 서버 간의 신뢰를 보장하기 위해 사용됩니다. 인증서는 서버 또는 클라이언트의 정체성을 증명하며, 데이터 암호화를 위한 공개 키를 포함합니다.

인증서 생성 절차


OpenSSL을 사용해 자체 서명(Self-Signed) 인증서를 생성할 수 있습니다.

  1. 개인 키 생성
    인증서에 사용될 RSA 개인 키를 생성합니다.
   openssl genrsa -out private.key 2048


이 명령어는 2048비트 길이의 개인 키를 private.key라는 파일에 저장합니다.

  1. CSR (Certificate Signing Request) 생성
    개인 키를 기반으로 인증서 서명 요청 파일을 생성합니다.
   openssl req -new -key private.key -out request.csr


실행 중 아래와 같은 정보를 입력해야 합니다:

  • 국가 이름(Country Name, 2글자 코드)
  • 주/도 이름(State or Province Name)
  • 도시 이름(Locality Name)
  • 조직 이름(Organization Name)
  • 조직 단위(Organizational Unit Name)
  • 도메인 이름(Common Name)
  • 이메일 주소(Email Address)
  1. 자체 서명 인증서 생성
    CSR 파일과 개인 키를 사용해 인증서를 생성합니다.
   openssl x509 -req -days 365 -in request.csr -signkey private.key -out certificate.crt


이 명령은 certificate.crt라는 파일에 1년 유효기간의 인증서를 생성합니다.

인증서 파일 설명

  • private.key: 인증서의 개인 키 파일
  • request.csr: 인증서 서명 요청 파일
  • certificate.crt: 최종 인증서 파일

인증서 관리

  1. 유효기간 확인:
    인증서의 만료일을 확인합니다.
   openssl x509 -enddate -noout -in certificate.crt
  1. 인증서 상세 정보 확인:
    인증서에 포함된 세부 정보를 출력합니다.
   openssl x509 -in certificate.crt -text -noout
  1. 인증서 갱신:
    만료된 인증서를 갱신하려면 새로운 CSR을 생성하고, 기존 키를 재사용합니다.

프로그램에서 인증서 사용


SSL/TLS를 사용하는 서버에서는 인증서와 키를 로드해야 합니다.

#include <openssl/ssl.h>

SSL_CTX *initialize_ssl_context() {
    SSL_CTX *ctx = SSL_CTX_new(TLS_server_method());
    if (!ctx) {
        perror("Unable to create SSL context");
        exit(EXIT_FAILURE);
    }

    SSL_CTX_use_certificate_file(ctx, "certificate.crt", SSL_FILETYPE_PEM);
    SSL_CTX_use_PrivateKey_file(ctx, "private.key", SSL_FILETYPE_PEM);

    if (!SSL_CTX_check_private_key(ctx)) {
        fprintf(stderr, "Private key does not match the certificate\n");
        exit(EXIT_FAILURE);
    }

    return ctx;
}

보안 권장사항

  • 인증서와 개인 키 보안: 인증서와 키 파일은 암호화된 저장소에 보관합니다.
  • 신뢰할 수 있는 인증서 사용: 실무에서는 자체 서명 인증서 대신 공인 인증 기관(CA)에서 발급받은 인증서를 사용합니다.
  • 정기 갱신: 인증서 만료 전에 갱신 작업을 수행합니다.

위의 절차와 권장사항을 따르면 SSL/TLS 인증서를 효과적으로 생성하고 관리할 수 있습니다.

클라이언트-서버 통신 구현

SSL/TLS 서버 구현


C언어와 OpenSSL을 사용하여 SSL/TLS 서버를 구현하려면 다음 단계를 따릅니다.

  1. SSL 컨텍스트 초기화
    서버 프로그램에서 SSL 컨텍스트를 초기화합니다.
   SSL_CTX *initialize_ssl_context() {
       SSL_CTX *ctx = SSL_CTX_new(TLS_server_method());
       if (!ctx) {
           perror("Unable to create SSL context");
           exit(EXIT_FAILURE);
       }

       SSL_CTX_use_certificate_file(ctx, "certificate.crt", SSL_FILETYPE_PEM);
       SSL_CTX_use_PrivateKey_file(ctx, "private.key", SSL_FILETYPE_PEM);

       if (!SSL_CTX_check_private_key(ctx)) {
           fprintf(stderr, "Private key does not match the certificate\n");
           exit(EXIT_FAILURE);
       }

       return ctx;
   }
  1. 소켓 설정 및 연결 대기
    서버는 TCP 소켓을 생성하고 클라이언트 연결을 대기합니다.
   int create_server_socket(int port) {
       int sockfd;
       struct sockaddr_in addr;

       sockfd = socket(AF_INET, SOCK_STREAM, 0);
       if (sockfd < 0) {
           perror("Unable to create socket");
           exit(EXIT_FAILURE);
       }

       addr.sin_family = AF_INET;
       addr.sin_port = htons(port);
       addr.sin_addr.s_addr = INADDR_ANY;

       if (bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
           perror("Unable to bind socket");
           exit(EXIT_FAILURE);
       }

       if (listen(sockfd, 1) < 0) {
           perror("Unable to listen on socket");
           exit(EXIT_FAILURE);
       }

       return sockfd;
   }
  1. SSL 연결 처리
    클라이언트가 연결되면 SSL 핸드셰이크를 수행합니다.
   void handle_ssl_connection(SSL *ssl) {
       if (SSL_accept(ssl) <= 0) {
           ERR_print_errors_fp(stderr);
       } else {
           printf("Client connected with SSL\n");
           char buffer[1024] = {0};
           SSL_read(ssl, buffer, sizeof(buffer));
           printf("Received: %s\n", buffer);
           SSL_write(ssl, "Hello, SSL client!", 18);
       }
       SSL_shutdown(ssl);
       SSL_free(ssl);
   }

SSL/TLS 클라이언트 구현


클라이언트 측에서도 SSL/TLS를 초기화하고 서버와 안전한 연결을 설정해야 합니다.

  1. SSL 컨텍스트 초기화
    클라이언트는 TLS 클라이언트 모드를 설정합니다.
   SSL_CTX *initialize_ssl_context() {
       SSL_CTX *ctx = SSL_CTX_new(TLS_client_method());
       if (!ctx) {
           perror("Unable to create SSL context");
           exit(EXIT_FAILURE);
       }
       return ctx;
   }
  1. 서버 연결 설정
    클라이언트 소켓을 생성하고 서버에 연결합니다.
   int create_client_socket(const char *hostname, int port) {
       int sockfd;
       struct sockaddr_in addr;

       sockfd = socket(AF_INET, SOCK_STREAM, 0);
       if (sockfd < 0) {
           perror("Unable to create socket");
           exit(EXIT_FAILURE);
       }

       struct hostent *host = gethostbyname(hostname);
       if (!host) {
           perror("Unable to resolve hostname");
           exit(EXIT_FAILURE);
       }

       addr.sin_family = AF_INET;
       addr.sin_port = htons(port);
       memcpy(&addr.sin_addr.s_addr, host->h_addr, host->h_length);

       if (connect(sockfd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
           perror("Unable to connect to server");
           exit(EXIT_FAILURE);
       }

       return sockfd;
   }
  1. SSL 통신 처리
    클라이언트는 서버와 데이터를 주고받습니다.
   void handle_ssl_communication(SSL *ssl) {
       SSL_write(ssl, "Hello, SSL server!", 18);
       char buffer[1024] = {0};
       SSL_read(ssl, buffer, sizeof(buffer));
       printf("Received: %s\n", buffer);
   }

통합 코드 예제


서버와 클라이언트 코드 각각을 실행하면 안전한 SSL/TLS 기반 통신이 가능합니다. SSL 초기화, 핸드셰이크, 데이터 송수신이 제대로 작동하는지 확인하며 개발합니다.

이로써 C언어 기반 SSL/TLS 클라이언트-서버 통신이 구현됩니다.

데이터 암호화와 복호화 처리

SSL/TLS에서 데이터 암호화


SSL/TLS는 데이터를 안전하게 보호하기 위해 암호화를 사용합니다. 이 과정은 핸드셰이크 단계에서 협상된 대칭 키를 기반으로 수행되며, 암호화된 데이터는 전송 중 해독이 불가능합니다.

암호화된 데이터 송수신


SSL/TLS는 데이터를 암호화하여 송수신하며, OpenSSL 라이브러리를 통해 이를 간단히 구현할 수 있습니다.

  1. SSL_write
    암호화된 데이터를 전송합니다.
   const char *message = "Hello, secure world!";
   int bytes_sent = SSL_write(ssl, message, strlen(message));
   if (bytes_sent > 0) {
       printf("Sent: %s\n", message);
   } else {
       printf("SSL_write error: %d\n", SSL_get_error(ssl, bytes_sent));
   }
  1. SSL_read
    암호화된 데이터를 수신하고 복호화합니다.
   char buffer[1024] = {0};
   int bytes_received = SSL_read(ssl, buffer, sizeof(buffer) - 1);
   if (bytes_received > 0) {
       printf("Received: %s\n", buffer);
   } else {
       printf("SSL_read error: %d\n", SSL_get_error(ssl, bytes_received));
   }

SSL/TLS의 암호화 알고리즘


SSL/TLS는 대칭 키 암호화와 공개 키 암호화를 결합하여 보안성을 유지합니다.

  • 공개 키 암호화: 핸드셰이크 단계에서 사용됩니다. 클라이언트와 서버는 공개 키를 교환하여 대칭 키를 안전하게 협상합니다.
  • 대칭 키 암호화: 데이터 전송 중 암호화와 복호화에 사용됩니다. AES, ChaCha20 등의 알고리즘이 일반적으로 사용됩니다.

OpenSSL을 활용한 직접 암호화


OpenSSL은 SSL/TLS 통신 외에도 직접 데이터 암호화/복호화를 지원합니다.

  1. AES 대칭 키 암호화
    OpenSSL의 EVP_CIPHER_CTX API를 사용하여 데이터를 암호화합니다.
   void encrypt_data(const unsigned char *key, const unsigned char *iv, 
                     const unsigned char *plaintext, unsigned char *ciphertext) {
       EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
       EVP_EncryptInit_ex(ctx, EVP_aes_256_cbc(), NULL, key, iv);

       int len;
       EVP_EncryptUpdate(ctx, ciphertext, &len, plaintext, strlen((char *)plaintext));
       EVP_EncryptFinal_ex(ctx, ciphertext + len, &len);

       EVP_CIPHER_CTX_free(ctx);
   }
  1. AES 대칭 키 복호화
    암호화된 데이터를 복호화합니다.
   void decrypt_data(const unsigned char *key, const unsigned char *iv, 
                     const unsigned char *ciphertext, unsigned char *plaintext) {
       EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
       EVP_DecryptInit_ex(ctx, EVP_aes_256_cbc(), NULL, key, iv);

       int len;
       EVP_DecryptUpdate(ctx, plaintext, &len, ciphertext, strlen((char *)ciphertext));
       EVP_DecryptFinal_ex(ctx, plaintext + len, &len);

       EVP_CIPHER_CTX_free(ctx);
   }

보안 권장사항

  • 강력한 암호화 알고리즘 사용: AES-256, ChaCha20와 같은 강력한 대칭 키 알고리즘을 선택합니다.
  • 안전한 키 관리: 대칭 키 및 공개 키는 안전한 방식으로 저장하고 교환해야 합니다.
  • TLS 최신 버전 사용: TLS 1.3은 더 강력한 보안과 효율성을 제공하므로 가능한 최신 버전을 채택합니다.

실행 결과 확인


암호화 및 복호화 과정이 올바르게 작동하는지 확인하기 위해 송수신된 데이터가 원래 메시지와 동일한지 테스트합니다. 이를 통해 SSL/TLS 기반 데이터 보호를 성공적으로 구현할 수 있습니다.

통신 오류 디버깅 및 해결 방법

SSL/TLS 통신에서 발생할 수 있는 주요 오류


SSL/TLS 통신을 구현하는 과정에서 다양한 오류가 발생할 수 있습니다. 주요 오류는 다음과 같습니다.

  1. SSL 핸드셰이크 실패
    클라이언트와 서버 간 핸드셰이크가 실패하면 연결이 설정되지 않습니다.
  • 원인: 인증서 문제, 불일치하는 프로토콜, 잘못된 키 파일 등
  1. 인증서 유효성 실패
    클라이언트가 서버의 인증서를 신뢰하지 못할 경우 발생합니다.
  • 원인: 인증서가 자가 서명되었거나, 인증 기관(CA)에서 발급되지 않음
  1. 프로토콜 버전 불일치
    클라이언트와 서버가 지원하는 SSL/TLS 버전이 다를 경우 발생합니다.
  • 원인: 서버는 TLS 1.2만 지원하는데, 클라이언트는 TLS 1.3으로 연결 시도
  1. 데이터 송수신 오류
    암호화된 데이터가 송수신 중 손상되거나 해독되지 않을 때 발생합니다.
  • 원인: 잘못된 암호화 키, 네트워크 문제

오류 디버깅 도구


OpenSSL은 SSL/TLS 통신을 디버깅하는 데 유용한 도구와 옵션을 제공합니다.

  1. SSL 핸드셰이크 디버깅 활성화
    OpenSSL의 디버깅 메시지를 활성화하여 문제의 원인을 파악합니다.
   SSL_CTX_set_info_callback(ctx, SSL_debug_callback);
  1. 명령줄 디버깅
    OpenSSL 명령줄 도구를 사용하여 연결 상태를 점검합니다.
   openssl s_client -connect <server>:<port>
  • 서버와 클라이언트 간의 인증서 상태 및 핸드셰이크 과정을 확인할 수 있습니다.

주요 문제 해결 방법

  1. SSL 핸드셰이크 실패 해결
  • 인증서와 개인 키가 일치하는지 확인합니다.
  • 클라이언트와 서버에서 동일한 SSL/TLS 프로토콜 버전을 사용합니다.
  • openssl verify 명령어로 인증서를 검증합니다.
    bash openssl verify -CAfile ca-cert.pem server-cert.pem
  1. 인증서 유효성 오류 해결
  • 신뢰할 수 있는 인증 기관(CA)에서 발급된 인증서를 사용합니다.
  • 자가 서명 인증서를 사용하는 경우, 클라이언트에 CA 인증서를 설치합니다.
  1. 프로토콜 버전 문제 해결
  • 서버와 클라이언트가 동일한 버전을 지원하도록 설정합니다.
   SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION);
   SSL_CTX_set_max_proto_version(ctx, TLS1_3_VERSION);
  1. 데이터 송수신 오류 해결
  • 암호화 키와 초기화 벡터(IV)가 정확히 일치하는지 확인합니다.
  • 네트워크 연결 상태를 점검하고 패킷 손실 여부를 확인합니다.

코드에서 오류 처리


OpenSSL 함수는 대부분 반환 값을 통해 오류를 나타냅니다.

  • SSL_acceptSSL_connect
    핸드셰이크 과정에서 반환 값을 확인합니다.
   if (SSL_accept(ssl) <= 0) {
       ERR_print_errors_fp(stderr);
   }
  • SSL_readSSL_write
    데이터 송수신 중 오류 발생 시 로그를 출력합니다.
   int ret = SSL_read(ssl, buffer, sizeof(buffer));
   if (ret <= 0) {
       int err = SSL_get_error(ssl, ret);
       printf("SSL_read error: %d\n", err);
   }

디버깅 팁

  • SSL/TLS의 모든 오류 메시지를 출력하려면 OpenSSL의 ERR_print_errors_fp를 활용합니다.
  • Wireshark와 같은 네트워크 분석 도구를 사용하여 패킷을 캡처하고 TLS 세션을 분석합니다.

보안 관련 주의사항

  • SSL/TLS 디버깅 시 실제 환경의 민감한 데이터를 노출하지 않도록 주의합니다.
  • 오류를 수정할 때 최신 보안 프로토콜과 인증서를 사용하는 것을 권장합니다.

이와 같은 방법을 통해 SSL/TLS 통신 오류를 디버깅하고 해결하여 안정적인 보안 통신을 구축할 수 있습니다.

응용 예제 및 실습

SSL/TLS를 활용한 파일 전송 프로그램


다음은 SSL/TLS를 사용하여 클라이언트와 서버 간에 파일을 안전하게 전송하는 C언어 기반 프로그램의 간단한 예제입니다.

서버 코드


서버는 SSL/TLS 연결을 설정하고 클라이언트로부터 파일 데이터를 수신합니다.

#include <openssl/ssl.h>
#include <openssl/err.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>

#define PORT 4443
#define BUFFER_SIZE 1024

void initialize_openssl() {
    SSL_library_init();
    SSL_load_error_strings();
    OpenSSL_add_all_algorithms();
}

void cleanup_openssl() {
    EVP_cleanup();
}

SSL_CTX *create_server_context() {
    SSL_CTX *ctx = SSL_CTX_new(TLS_server_method());
    if (!ctx) {
        perror("Unable to create SSL context");
        exit(EXIT_FAILURE);
    }

    if (SSL_CTX_use_certificate_file(ctx, "server.crt", SSL_FILETYPE_PEM) <= 0 ||
        SSL_CTX_use_PrivateKey_file(ctx, "server.key", SSL_FILETYPE_PEM) <= 0) {
        ERR_print_errors_fp(stderr);
        exit(EXIT_FAILURE);
    }

    return ctx;
}

void handle_client(SSL *ssl) {
    FILE *file = fopen("received_file.txt", "wb");
    if (!file) {
        perror("Unable to open file");
        return;
    }

    char buffer[BUFFER_SIZE] = {0};
    int bytes;
    while ((bytes = SSL_read(ssl, buffer, sizeof(buffer))) > 0) {
        fwrite(buffer, 1, bytes, file);
    }

    fclose(file);
    printf("File received successfully.\n");
}

int main() {
    initialize_openssl();
    SSL_CTX *ctx = create_server_context();

    int server_sock = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in addr = {
        .sin_family = AF_INET,
        .sin_port = htons(PORT),
        .sin_addr.s_addr = INADDR_ANY,
    };

    bind(server_sock, (struct sockaddr *)&addr, sizeof(addr));
    listen(server_sock, 1);

    printf("Server listening on port %d...\n", PORT);

    int client_sock = accept(server_sock, NULL, NULL);
    SSL *ssl = SSL_new(ctx);
    SSL_set_fd(ssl, client_sock);

    if (SSL_accept(ssl) <= 0) {
        ERR_print_errors_fp(stderr);
    } else {
        handle_client(ssl);
    }

    SSL_free(ssl);
    close(client_sock);
    close(server_sock);
    SSL_CTX_free(ctx);
    cleanup_openssl();

    return 0;
}

클라이언트 코드


클라이언트는 서버에 연결한 뒤 파일 데이터를 전송합니다.

#include <openssl/ssl.h>
#include <openssl/err.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>

#define PORT 4443
#define BUFFER_SIZE 1024

void initialize_openssl() {
    SSL_library_init();
    SSL_load_error_strings();
    OpenSSL_add_all_algorithms();
}

void cleanup_openssl() {
    EVP_cleanup();
}

SSL_CTX *create_client_context() {
    SSL_CTX *ctx = SSL_CTX_new(TLS_client_method());
    if (!ctx) {
        perror("Unable to create SSL context");
        exit(EXIT_FAILURE);
    }
    return ctx;
}

void send_file(SSL *ssl, const char *filename) {
    FILE *file = fopen(filename, "rb");
    if (!file) {
        perror("Unable to open file");
        return;
    }

    char buffer[BUFFER_SIZE] = {0};
    int bytes;
    while ((bytes = fread(buffer, 1, sizeof(buffer), file)) > 0) {
        SSL_write(ssl, buffer, bytes);
    }

    fclose(file);
    printf("File sent successfully.\n");
}

int main() {
    initialize_openssl();
    SSL_CTX *ctx = create_client_context();

    int client_sock = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in addr = {
        .sin_family = AF_INET,
        .sin_port = htons(PORT),
        .sin_addr.s_addr = inet_addr("127.0.0.1"),
    };

    if (connect(client_sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
        perror("Unable to connect to server");
        exit(EXIT_FAILURE);
    }

    SSL *ssl = SSL_new(ctx);
    SSL_set_fd(ssl, client_sock);

    if (SSL_connect(ssl) <= 0) {
        ERR_print_errors_fp(stderr);
    } else {
        send_file(ssl, "sample_file.txt");
    }

    SSL_free(ssl);
    close(client_sock);
    SSL_CTX_free(ctx);
    cleanup_openssl();

    return 0;
}

실행 방법

  1. 서버 실행: ./server
  2. 클라이언트 실행: ./client

결과 확인


클라이언트가 전송한 파일(sample_file.txt)이 서버의 디렉토리에 received_file.txt로 저장됩니다.

응용 및 확장

  • 다중 클라이언트 지원: 서버에서 다중 클라이언트를 처리하도록 스레드를 추가합니다.
  • 파일 암호화: 전송 전 추가적인 데이터 암호화를 적용합니다.
  • 인증서 검증: 클라이언트가 서버 인증서를 검증하도록 설정합니다.

위 예제를 활용하면 SSL/TLS를 사용한 안전한 파일 전송 프로그램을 구현할 수 있습니다.

요약


SSL/TLS를 활용해 C언어에서 안전한 통신을 구현하는 방법을 학습했습니다. SSL/TLS의 기본 개념부터 인증서 생성, 클라이언트-서버 통신 구현, 데이터 암호화, 오류 디버깅, 파일 전송 예제까지 다루어 실제 네트워크 애플리케이션에서 보안 통신을 구현할 수 있는 기반을 마련했습니다. 이를 통해 안전하고 신뢰할 수 있는 소프트웨어 개발이 가능합니다.

목차