C++ 보안 코딩 가이드: 오버플로우 방지와 암호화 라이브러리(OpenSSL) 실전 연동 [#43-2]

C++ 보안 코딩 가이드: 오버플로우 방지와 암호화 라이브러리(OpenSSL) 실전 연동 [#43-2]

이 글의 핵심

C++ 보안 코딩 가이드: 오버플로우 방지와 암호화 라이브러리(OpenSSL) 실전 연동 [#43-2]에 대한 실전 가이드입니다.

들어가며: “일단 돌아가게”가 보안 버그를 만든다

오버플로우와 암호화 실수

30번에서 SSL/TLS를 다뤘다면, 43-2는 보안 코딩에 집중합니다. 정수 오버플로우(연산 결과가 타입 범위를 넘는 것)·버퍼 오버플로우(할당된 영역 밖으로 쓰는 것)는 메모리 손상·임의 코드 실행으로 이어질 수 있고, 암호화 사용 실수(난수 품질·키 관리·패딩)는 통신이 탈취되거나 위조될 수 있습니다.
C++에서는 크기 계산 시 checked 연산·안전한 API를 쓰고, OpenSSL을 쓸 때는 RAII로 핸들을 감싸고 에러 코드를 반드시 확인하는 습관이 필요합니다.

이 글에서 다루는 것:

  • 오버플로우 방지: 정수 연산·버퍼 크기 검사·안전한 함수
  • OpenSSL: 초기화·에러 처리·RAII 래퍼
  • 실전: 난수·키·TLS 설정·HMAC·AES-GCM·프로덕션 패턴

실제 문제 시나리오

시나리오 1: 할당 크기 오버플로우로 인한 힙 버퍼 오버플로우

사용자 입력: count=1000000, item_size=4096
개발자 코드: buffer = malloc(count * item_size);
결과: count * item_size가 size_t 범위를 넘어 0 또는 작은 값으로 래핑
→ malloc(작은 크기) 후 대량 쓰기 → 힙 손상, RCE

시나리오 2: rand()로 생성한 세션 토큰 예측

개발자: session_token = rand() % 1000000;
공격자: rand() 시드가 time(NULL)이면 1초 내 가능한 값만 브루트포스
→ 세션 하이재킹

시나리오 3: EVP 함수 반환값 미검사

개발자: EVP_EncryptUpdate(ctx, out, &len, in, in_len);  // 반환값 무시
실제: 내부 에러 시 0 반환, out에 쓰레기 또는 부분 암호문
→ 복호화 실패, 데이터 손상, 또는 정보 유출

시나리오 4: SSL_CTX_set_verify 모드 생략

개발자: SSL_CTX만 생성하고 verify 모드 설정 안 함
결과: 기본값에 따라 인증서 검증이 건너뛰어질 수 있음
→ MITM 공격에 취약

이 글에서는 위와 같은 문제를 예방하는 패턴과 완전한 예제를 다룹니다.

목차

  1. 오버플로우 방지
  2. OpenSSL C++ 연동
  3. 완전한 보안 코딩 예제
  4. 일반적인 취약점
  5. 모범 사례
  6. 프로덕션 패턴
  7. 자주 발생하는 에러와 해결법
  8. 구현 체크리스트
  9. 정리

개념을 잡는 비유

소켓과 비동기 I/O는 우편함 주소와 배달 경로로 이해하면 편합니다. 주소(IP·포트)만 맞으면 데이터가 들어오고, Asio는 한 우체국에서 여러 배달부(스레드·핸들러)가 일을 나누는 구조로 보시면 됩니다.


1. 오버플로우 방지

정수·버퍼 안전

  • 정수 오버플로우: a + b가 타입 범위를 넘으면 undefined behavior입니다. 할당 크기·인덱스 계산 전에 overflow check를 하거나, std::numeric_limits로 상한을 검사합니다. C++20 std::in_range 등도 활용할 수 있습니다.
  • 버퍼 오버플로우: 길이 제한 없는 sprintf, strcpy 대신 snprintf, strncpy(또는 std::string·std::span)를 사용하고, 쓰기 전크기 <= 버퍼 크기인지 확인합니다. 정적 분석(41-1)과 Sanitizer(41-2, UBSan)로 오버플로우를 찾을 수 있습니다.
  • 배열 인덱스: size_t로 인덱스를 받을 때 음수가 들어오지 않도록 타입·검증을 하고, bounds check 후 접근합니다.

오버플로우 방지 코드 예제

정수 오버플로우 체크 (안전한 할당 크기 계산):

#include <limits>
#include <cstddef>
#include <stdexcept>

// a * b가 오버플로우 없이 계산 가능한지 검사
bool safe_multiply(size_t a, size_t b, size_t& out) {
    if (a == 0 || b == 0) {
        out = 0;
        return true;
    }
    if (a > std::numeric_limits<size_t>::max() / b)
        return false;
    out = a * b;
    return true;
}

// 사용 예: 버퍼 할당 전 검사
void allocate_buffer(size_t count, size_t item_size) {
    size_t total;
    if (!safe_multiply(count, item_size, total))
        throw std::overflow_error("allocation size overflow");
    auto buf = std::make_unique<char[]>(total);
    // ...
}

버퍼 쓰기 전 크기 검증:

#include <cstdio>
#include <cstdarg>
#include <span>
#include <string_view>

// ❌ 위험: 크기 제한 없음
void bad_copy(char* dest, const char* src) {
    // strcpy(dest, src);  // 버퍼 오버플로우
}

// ✅ 안전: snprintf 또는 std::string 사용
bool safe_format(char* dest, size_t dest_size, const char* fmt, ...) {
    if (dest_size == 0) return false;
    va_list args;
    va_start(args, fmt);
    int n = vsnprintf(dest, dest_size, fmt, args);
    va_end(args);
    return n >= 0 && static_cast<size_t>(n) < dest_size;
}

// ✅ C++17: std::span으로 범위 명시
void process_span(std::span<const uint8_t> data) {
    for (size_t i = 0; i < data.size(); ++i) {
        // bounds-safe 접근
    }
}

배열 인덱스 검증:

#include <vector>
#include <cassert>

template<typename T>
T& safe_at(std::vector<T>& v, size_t i) {
    if (i >= v.size())
        throw std::out_of_range("index out of range");
    return v[i];
}

// size_t로 받을 때 음수 방지: signed를 받으면 검증
template<typename T>
T& safe_at_signed(std::vector<T>& v, ptrdiff_t i) {
    if (i < 0 || static_cast<size_t>(i) >= v.size())
        throw std::out_of_range("index out of range");
    return v[static_cast<size_t>(i)];
}

2. OpenSSL C++ 연동

초기화·에러·RAII

  • OpenSSL은 C API라서 에러 큐를 수동으로 확인해야 합니다. ERR_get_error()로 오류 코드를 꺼내고 ERR_error_string()으로 메시지를 얻습니다. 모든 반환값을 검사하고, 실패 시 정리 후 조기 반환이 원칙입니다.
  • 리소스: EVP_*, SSL_CTX, BIO 등은 할당 후 반드시 해제해야 합니다. C++에서는 RAII 클래스로 감싸서 소멸자에서 free 함수를 호출하면 누수를 방지할 수 있습니다. unique_ptrCustom Deleter를 주는 방식이 간단합니다.
  • 초기화: OpenSSL 3.x에서는 OPENSSL_init_ssl 등을 호출해야 합니다. thread safety는 기본으로 제공되지만, 옵션에 따라 다르므로 문서를 확인합니다.

EVP_PKEY_Deleteroperator()(EVP_PKEY* p)에서 EVP_PKEY_free(p)를 호출하는 펑터라서, unique_ptr가 소멸될 때 자동으로 EVP_PKEY_free가 호출됩니다. EVP_PKEY_new()가 실패하면 nullptr를 반환하므로 if (!key)로 검사한 뒤 ERR_get_error()로 OpenSSL 에러 큐를 확인하면 됩니다. 다른 OpenSSL 타입도 같은 방식으로 XXX_free를 Deleter로 넣어 UniqueXXX 별칭을 두면 됩니다.

RAII 래퍼 정의

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

struct EVP_PKEY_Deleter { void operator()(EVP_PKEY* p) const { EVP_PKEY_free(p); } };
using UniqueEVP_PKEY = std::unique_ptr<EVP_PKEY, EVP_PKEY_Deleter>;

struct EVP_MD_CTX_Deleter { void operator()(EVP_MD_CTX* p) const { EVP_MD_CTX_free(p); } };
using UniqueEVP_MD_CTX = std::unique_ptr<EVP_MD_CTX, EVP_MD_CTX_Deleter>;

struct EVP_CIPHER_CTX_Deleter { void operator()(EVP_CIPHER_CTX* p) const { EVP_CIPHER_CTX_free(p); } };
using UniqueEVP_CIPHER_CTX = std::unique_ptr<EVP_CIPHER_CTX, EVP_CIPHER_CTX_Deleter>;

struct SSL_CTX_Deleter { void operator()(SSL_CTX* p) const { SSL_CTX_free(p); } };
using UniqueSSL_CTX = std::unique_ptr<SSL_CTX, SSL_CTX_Deleter>;

struct BIO_Deleter { void operator()(BIO* p) const { BIO_free(p); } };
using UniqueBIO = std::unique_ptr<BIO, BIO_Deleter>;

UniqueEVP_PKEY key(EVP_PKEY_new());
if (!key) {
    // ERR_get_error(); 로 에러 큐 확인
    return;
}

에러 큐 확인 유틸리티

#include <string>
#include <sstream>

std::string get_openssl_errors() {
    std::ostringstream oss;
    unsigned long err;
    while ((err = ERR_get_error()) != 0) {
        char buf[256];
        ERR_error_string_n(err, buf, sizeof(buf));
        oss << buf << "; ";
    }
    return oss.str();
}

// 사용 예
int ret = EVP_EncryptUpdate(ctx, out, &len, in, in_len);
if (ret != 1) {
    std::cerr << "EVP_EncryptUpdate failed: " << get_openssl_errors() << "\n";
    return -1;
}

OpenSSL 초기화 (3.x)

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

void init_openssl() {
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
    if (OPENSSL_init_ssl(OPENSSL_INIT_LOAD_SSL_STRINGS | OPENSSL_INIT_LOAD_CRYPTO_STRINGS, nullptr) != 1) {
        // 초기화 실패
    }
#else
    SSL_library_init();
    SSL_load_error_strings();
    OpenSSL_add_all_algorithms();
#endif
}

3. 완전한 보안 코딩 예제

암호학적 난수 생성

#include <openssl/rand.h>
#include <vector>
#include <stdexcept>

std::vector<uint8_t> secure_random_bytes(size_t n) {
    std::vector<uint8_t> buf(n);
    if (RAND_bytes(buf.data(), static_cast<int>(n)) != 1) {
        throw std::runtime_error("RAND_bytes failed");
    }
    return buf;
}

// 세션 토큰 생성 (32바이트 = 256비트)
std::vector<uint8_t> generate_session_token() {
    return secure_random_bytes(32);
}

HMAC-SHA256 (메시지 무결성 검증)

#include <openssl/evp.h>
#include <openssl/hmac.h>
#include <openssl/err.h>
#include <vector>
#include <cstring>
#include <stdexcept>

std::vector<uint8_t> hmac_sha256(const uint8_t* key, size_t key_len,
                                 const uint8_t* data, size_t data_len) {
    unsigned int len = 0;
    std::vector<uint8_t> out(EVP_MAX_MD_SIZE);

    unsigned char* result = HMAC(EVP_sha256(), key, static_cast<int>(key_len),
                                 data, static_cast<int>(data_len),
                                 out.data(), &len);
    if (!result) {
        throw std::runtime_error("HMAC failed");
    }
    out.resize(len);
    return out;
}

// 헬퍼: 바이트를 hex 문자열로
std::string bytes_to_hex(const std::vector<uint8_t>& bytes) {
    static const char hex[] = "0123456789abcdef";
    std::string s;
    for (uint8_t b : bytes) {
        s += hex[b >> 4];
        s += hex[b & 0xf];
    }
    return s;
}

// 사용 예: API 서명 검증 (CRYPTO_memcmp로 타이밍 공격 방지)
bool verify_api_signature(const std::string& secret,
                          const std::string& payload,
                          const std::string& received_signature_hex) {
    auto mac = hmac_sha256(
        reinterpret_cast<const uint8_t*>(secret.data()), secret.size(),
        reinterpret_cast<const uint8_t*>(payload.data()), payload.size());
    std::string computed_hex = bytes_to_hex(mac);
    return computed_hex.size() == received_signature_hex.size() &&
           CRYPTO_memcmp(computed_hex.data(), received_signature_hex.data(),
                         computed_hex.size()) == 0;
}

AES-256-GCM 대칭 암호화 (인증 암호화)

#include <openssl/evp.h>
#include <openssl/rand.h>
#include <openssl/err.h>
#include <vector>
#include <stdexcept>
#include <cstring>

struct AesGcmResult {
    std::vector<uint8_t> ciphertext;
    std::vector<uint8_t> tag;  // 16 bytes for GCM
    std::vector<uint8_t> iv;   // 12 bytes recommended for GCM
};

AesGcmResult aes_gcm_encrypt(const uint8_t* key, size_t key_len,
                              const uint8_t* plaintext, size_t plain_len,
                              const uint8_t* aad, size_t aad_len) {
    if (key_len != 32) throw std::invalid_argument("key must be 32 bytes");

    EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
    if (!ctx) throw std::runtime_error("EVP_CIPHER_CTX_new failed");

    AesGcmResult result;
    result.iv.resize(12);
    if (RAND_bytes(result.iv.data(), 12) != 1) {
        EVP_CIPHER_CTX_free(ctx);
        throw std::runtime_error("RAND_bytes failed");
    }

    if (EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), nullptr, key, result.iv.data()) != 1) {
        EVP_CIPHER_CTX_free(ctx);
        throw std::runtime_error("EVP_EncryptInit_ex failed");
    }

    if (aad_len > 0 && EVP_EncryptUpdate(ctx, nullptr, nullptr, aad, static_cast<int>(aad_len)) != 1) {
        EVP_CIPHER_CTX_free(ctx);
        throw std::runtime_error("EVP_EncryptUpdate AAD failed");
    }

    result.ciphertext.resize(plain_len + EVP_CIPHER_block_size(EVP_aes_256_gcm()));
    int out_len = 0;
    if (EVP_EncryptUpdate(ctx, result.ciphertext.data(), &out_len, plaintext, static_cast<int>(plain_len)) != 1) {
        EVP_CIPHER_CTX_free(ctx);
        throw std::runtime_error("EVP_EncryptUpdate failed");
    }
    result.ciphertext.resize(out_len);

    int final_len = 0;
    if (EVP_EncryptFinal_ex(ctx, result.ciphertext.data() + out_len, &final_len) != 1) {
        EVP_CIPHER_CTX_free(ctx);
        throw std::runtime_error("EVP_EncryptFinal_ex failed");
    }
    result.ciphertext.resize(out_len + final_len);

    result.tag.resize(16);
    if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, 16, result.tag.data()) != 1) {
        EVP_CIPHER_CTX_free(ctx);
        throw std::runtime_error("EVP_CTRL_GCM_GET_TAG failed");
    }

    EVP_CIPHER_CTX_free(ctx);
    return result;
}

TLS 클라이언트 컨텍스트 설정 (보안 강화)

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

SSL_CTX* create_secure_client_ctx() {
    const SSL_METHOD* method = TLS_client_method();
    SSL_CTX* ctx = SSL_CTX_new(method);
    if (!ctx) return nullptr;

    // 인증서 검증 필수
    SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, nullptr);
    SSL_CTX_set_verify_depth(ctx, 5);

    // 시스템 CA 저장소 로드
    if (SSL_CTX_set_default_verify_paths(ctx) != 1) {
        SSL_CTX_free(ctx);
        return nullptr;
    }

    // 약한 프로토콜/암호 스위트 비활성화
    SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION);
    if (SSL_CTX_set_ciphersuites(ctx, "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256") != 1) {
        SSL_CTX_free(ctx);
        return nullptr;
    }

    return ctx;
}

시큐어 메모리 제로화

#include <openssl/crypto.h>

void secure_zero(void* ptr, size_t len) {
    OPENSSL_cleanse(ptr, len);
}

// 사용 예: 키 사용 후 제로화
void use_key_then_clear(std::vector<uint8_t>& key) {
    // ... 키 사용 ...
    secure_zero(key.data(), key.size());
    key.clear();
}

memset vs OPENSSL_cleanse: memset(ptr, 0, len)은 컴파일러가 “dead store”로 판단해 최적화로 제거할 수 있습니다. OPENSSL_cleanse는 메모리를 안전하게 덮어쓰고 최적화되지 않도록 설계되었습니다.

AES-GCM 복호화 (태그 검증 포함)

std::vector<uint8_t> aes_gcm_decrypt(const uint8_t* key, size_t key_len,
                                     const uint8_t* iv, size_t iv_len,
                                     const uint8_t* ciphertext, size_t cipher_len,
                                     const uint8_t* tag, size_t tag_len,
                                     const uint8_t* aad, size_t aad_len) {
    if (key_len != 32 || iv_len != 12 || tag_len != 16)
        throw std::invalid_argument("invalid key/iv/tag length");

    EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
    if (!ctx) throw std::runtime_error("EVP_CIPHER_CTX_new failed");

    if (EVP_DecryptInit_ex(ctx, EVP_aes_256_gcm(), nullptr, key, iv) != 1) {
        EVP_CIPHER_CTX_free(ctx);
        throw std::runtime_error("EVP_DecryptInit_ex failed");
    }

    if (aad_len > 0 && EVP_DecryptUpdate(ctx, nullptr, nullptr, aad, static_cast<int>(aad_len)) != 1) {
        EVP_CIPHER_CTX_free(ctx);
        throw std::runtime_error("EVP_DecryptUpdate AAD failed");
    }

    std::vector<uint8_t> plaintext(cipher_len);
    int out_len = 0;
    if (EVP_DecryptUpdate(ctx, plaintext.data(), &out_len, ciphertext, static_cast<int>(cipher_len)) != 1) {
        EVP_CIPHER_CTX_free(ctx);
        throw std::runtime_error("EVP_DecryptUpdate failed");
    }

    // 태그 설정: 반드시 DecryptFinal 전에 호출
    if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, 16, const_cast<uint8_t*>(tag)) != 1) {
        EVP_CIPHER_CTX_free(ctx);
        throw std::runtime_error("EVP_CTRL_GCM_SET_TAG failed");
    }

    int final_len = 0;
    if (EVP_DecryptFinal_ex(ctx, plaintext.data() + out_len, &final_len) != 1) {
        EVP_CIPHER_CTX_free(ctx);
        throw std::runtime_error("EVP_DecryptFinal_ex failed: tag mismatch (tampered?)");
    }
    plaintext.resize(out_len + final_len);
    EVP_CIPHER_CTX_free(ctx);
    return plaintext;
}

주의: EVP_DecryptFinal_ex가 실패하면 태그 불일치 = 데이터 변조를 의미합니다. 이 경우 평문을 사용하면 안 됩니다.

SHA-256 해시 (파일·메시지 무결성)

#include <openssl/evp.h>
#include <fstream>
#include <vector>

std::vector<uint8_t> sha256(const uint8_t* data, size_t len) {
    std::vector<uint8_t> out(EVP_MAX_MD_SIZE);
    unsigned int out_len = 0;

    EVP_MD_CTX* ctx = EVP_MD_CTX_new();
    if (!ctx) throw std::runtime_error("EVP_MD_CTX_new failed");

    if (EVP_DigestInit_ex(ctx, EVP_sha256(), nullptr) != 1 ||
        EVP_DigestUpdate(ctx, data, len) != 1 ||
        EVP_DigestFinal_ex(ctx, out.data(), &out_len) != 1) {
        EVP_MD_CTX_free(ctx);
        throw std::runtime_error("SHA256 failed");
    }
    EVP_MD_CTX_free(ctx);
    out.resize(out_len);
    return out;
}

4. 일반적인 취약점

취약점 1: ECB 모드 사용

문제: AES-ECB는 같은 평문 블록이 같은 암호문을 생성합니다. 패턴이 노출됩니다.

// ❌ 위험: ECB 모드
EVP_EncryptInit_ex(ctx, EVP_aes_256_ecb(), ...);

// ✅ 권장: GCM, CBC+HMAC 등 인증 암호화
EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), ...);

취약점 2: IV/Nonce 재사용

문제: GCM 등에서 같은 키+IV로 두 번 암호화하면 보안이 깨집니다.

// ❌ 위험: 고정 IV
uint8_t iv[12] = {0};

// ✅ 권장: 암호화마다 새 IV 생성
std::vector<uint8_t> iv(12);
RAND_bytes(iv.data(), 12);

취약점 3: 패딩 오라클 (CBC)

문제: CBC + PKCS7 패딩에서 복호화 에러 메시지로 패딩 유효성을 알 수 있으면 오라클 공격이 가능합니다.

// ✅ 권장: GCM 등 AEAD 사용으로 패딩 오라클 제거

취약점 4: 비교 시 타이밍 공격

문제: memcmp로 서명/토큰을 비교하면 바이트 단위로 조기 반환해 타이밍 차이로 추측 가능합니다.

// ❌ 위험
if (memcmp(computed, received, len) != 0) return false;

// ✅ 안전
if (CRYPTO_memcmp(computed, received, len) != 0) return false;

취약점 5: SSL_VERIFY_NONE

문제: 인증서 검증을 끄면 MITM에 취약합니다.

// ❌ 절대 사용 금지
SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, nullptr);

// ✅ 필수
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, nullptr);

5. 모범 사례

1. 모든 OpenSSL 반환값 검사

// 모든 EVP_*, SSL_*, RAND_* 등 반환값 확인
if (EVP_EncryptUpdate(ctx, out, &len, in, in_len) != 1) {
    // 에러 처리, 리소스 정리, 반환
}

2. RAII로 리소스 관리

UniqueEVP_CIPHER_CTX ctx(EVP_CIPHER_CTX_new());
if (!ctx) return -1;
// 예외 발생 시에도 EVP_CIPHER_CTX_free 자동 호출

3. 키는 최소 권한·최소 시간만 보관

{
    std::vector<uint8_t> key = load_key_from_secure_storage();
    do_encryption(key);
    secure_zero(key.data(), key.size());
}  // key 소멸

4. 알고리즘·버전 명시

// ✅ 명시적: TLS 1.2 이상, 강한 암호 스위트만
SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION);
SSL_CTX_set_ciphersuites(ctx, "TLS_AES_256_GCM_SHA384:...");

5. 로깅 시 민감 정보 제외

// ❌ 키/비밀/토큰 로깅 금지
// LOG("key=" << key);

// ✅ 에러 코드·상태만 로깅
LOG("EVP_EncryptUpdate failed, err=" << ERR_get_error());

6. 프로덕션 패턴

보안 초기화 플로우

flowchart TD
    A[프로그램 시작] --> B[OPENSSL_init_ssl]
    B --> C[초기화 성공?]
    C -->|No| D[로그 후 종료]
    C -->|Yes| E[SSL_CTX 생성]
    E --> F[verify=PEER, min=TLS1.2]
    F --> G[암호 스위트 제한]
    G --> H[CA 저장소 로드]
    H --> I[서비스 준비 완료]

키 로테이션 패턴

class KeyManager {
public:
    std::vector<uint8_t> get_current_key() const {
        std::lock_guard<std::mutex> lock(mutex_);
        return current_key_;
    }
    void rotate_key(const std::vector<uint8_t>& new_key) {
        std::lock_guard<std::mutex> lock(mutex_);
        secure_zero(current_key_.data(), current_key_.size());
        current_key_ = new_key;
    }
private:
    mutable std::mutex mutex_;
    std::vector<uint8_t> current_key_;
};

에러 처리 및 로깅 패턴

enum class CryptoResult {
    Ok,
    InitFailed,
    EncryptFailed,
    DecryptFailed,
};

CryptoResult encrypt_with_logging(const std::vector<uint8_t>& plaintext,
                                   std::vector<uint8_t>& ciphertext) {
    UniqueEVP_CIPHER_CTX ctx(EVP_CIPHER_CTX_new());
    if (!ctx) {
        LOG_ERROR("EVP_CIPHER_CTX_new failed: " << get_openssl_errors());
        return CryptoResult::InitFailed;
    }
    // ... 암호화 로직, 각 단계에서 실패 시 로그 후 반환
    return CryptoResult::Ok;
}

환경별 설정 분리

// 개발: 로컬 CA, 디버그 로깅
// 스테이징: 테스트 CA, 상세 로깅
// 프로덕션: 시스템 CA, 에러만 로깅, 민감 정보 절대 로깅 안 함

TLS 핸드셰이크 및 데이터 흐름

sequenceDiagram
    participant C as Client
    participant S as Server

    C->>S: ClientHello (TLS 1.2+, cipher suites)
    S->>C: ServerHello, Certificate, ServerHelloDone
    C->>C: 인증서 검증 (SSL_VERIFY_PEER)
    C->>S: ClientKeyExchange, ChangeCipherSpec, Finished
    S->>C: ChangeCipherSpec, Finished
    Note over C,S: 암호화된 애플리케이션 데이터

키 파생 (PBKDF2)

비밀번호에서 키를 파생할 때는 salt반복 횟수가 필수입니다.

#include <openssl/evp.h>
#include <openssl/rand.h>

std::vector<uint8_t> pbkdf2_sha256(const char* password, size_t pass_len,
                                    const uint8_t* salt, size_t salt_len,
                                    int iterations, size_t key_len) {
    std::vector<uint8_t> key(key_len);
    if (PKCS5_PBKDF2_HMAC(password, static_cast<int>(pass_len), salt,
                           static_cast<int>(salt_len), iterations, EVP_sha256(),
                           static_cast<int>(key_len), key.data()) != 1) {
        throw std::runtime_error("PBKDF2 failed");
    }
    return key;
}

// 사용 예: 비밀번호 + 랜덤 salt로 32바이트 키 파생
std::vector<uint8_t> derive_key_from_password(const std::string& password) {
    std::vector<uint8_t> salt(16);
    RAND_bytes(salt.data(), 16);
    return pbkdf2_sha256(password.data(), password.size(),
                          salt.data(), salt.size(), 100000, 32);
}

주의: salt는 암호문과 함께 저장해야 하며, 사용자/레코드마다 고유해야 합니다. 반복 횟수는 최소 100,000 이상 권장됩니다.


7. 자주 발생하는 에러와 해결법

문제 1: “EVP_EncryptUpdate failed” 또는 0 반환

원인: 키/IV 길이 오류, 컨텍스트 초기화 실패, 버퍼 크기 부족

해결법:

// 키 길이 확인 (AES-256 = 32바이트)
if (key.size() != 32) {
    throw std::invalid_argument("AES-256 requires 32-byte key");
}

// 출력 버퍼: plain_len + block_size(16) 이상
out.resize(plain_len + 16);
int out_len = 0;
int ret = EVP_EncryptUpdate(ctx, out.data(), &out_len, in, in_len);
if (ret != 1) {
    std::cerr << get_openssl_errors() << "\n";
    return -1;
}

문제 2: “SSL_connect failed” / 인증서 검증 실패

원인: CA 저장소 경로 오류, 만료/자체 서명 인증서, 호스트명 불일치

해결법:

// 시스템 CA 사용
SSL_CTX_set_default_verify_paths(ctx);

// 또는 명시적 CA 파일
SSL_CTX_load_verify_locations(ctx, "/etc/ssl/certs/ca-certificates.crt", nullptr);

// 호스트명 검증 (OpenSSL 1.1.1+)
SSL_set1_host(ssl, "example.com");

문제 3: RAND_bytes 실패

원인: /dev/urandom 접근 불가, 엔트로피 부족(드물음)

해결법:

if (RAND_bytes(buf, len) != 1) {
    // RAND_status()로 상태 확인
    if (RAND_status() != 1) {
        // 엔트로피 품질 부족, 재시도 또는 실패 처리
    }
    throw std::runtime_error("RAND_bytes failed");
}

문제 4: 메모리 누수 (EVP_*, SSL_CTX 미해제)

원인: 에러 경로에서 free 호출 누락

해결법: RAII 래퍼 사용으로 모든 경로에서 자동 해제

UniqueEVP_CIPHER_CTX ctx(EVP_CIPHER_CTX_new());
if (!ctx) return -1;
// 중간에 return/throw 해도 소멸자에서 free

문제 5: GCM 태그 검증 생략

원인: 복호화 후 태그 검증을 안 하면 변조 감지 불가

해결법:

// 복호화 후 반드시 태그 설정 및 검증
EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, 16, tag_from_ciphertext);
if (EVP_DecryptFinal_ex(ctx, out + out_len, &final_len) != 1) {
    // 태그 불일치 = 변조됨
    return -1;
}

문제 6: OpenSSL 3.x에서 “legacy” 알고리즘 오류

원인: OpenSSL 3.0부터 일부 알고리즘(MD5, DES 등)이 기본 비활성화됨

해결법:

// 필요 시 (레거시 호환용) 프로바이더 로드
#include <openssl/provider.h>

OSSL_PROVIDER* leg = OSSL_PROVIDER_load(nullptr, "legacy");
OSSL_PROVIDER* def = OSSL_PROVIDER_load(nullptr, "default");
// 사용 후 OSSL_PROVIDER_unload(leg);

권장: 레거시 알고리즘 대신 SHA-256, AES-GCM 등 현대 알고리즘으로 마이그레이션하는 것이 좋습니다.

문제 7: 스레드 안전성

원인: OpenSSL 1.1.0+는 기본 스레드 안전이지만, 에러 큐는 스레드별로 분리됩니다.

해결법: 각 스레드에서 ERR_get_error()를 호출하면 해당 스레드의 에러만 반환됩니다. 공유 SSL_CTX는 스레드 안전하게 사용 가능합니다.


8. 구현 체크리스트

오버플로우 방지

  • 할당 크기 계산 시 safe_multiply 등 오버플로우 검사
  • sprintf/strcpy 대신 snprintf/strncpy 또는 std::string
  • 인덱스 접근 전 bounds check
  • UBSan, ASan으로 빌드·테스트

OpenSSL 사용

  • OPENSSL_init_ssl (3.x) 또는 SSL_library_init 호출
  • 모든 EVP_*, SSL_*, RAND_* 반환값 검사
  • RAII로 EVP_*, SSL_CTX, BIO 등 해제
  • ERR_get_error()로 에러 큐 확인

암호화

  • rand() 대신 RAND_bytes 사용
  • ECB 대신 GCM, CBC+HMAC 등 AEAD
  • IV/Nonce 매번 새로 생성
  • GCM 태그 검증 필수
  • memcmp 대신 CRYPTO_memcmp로 상수 시간 비교

TLS

  • SSL_VERIFY_PEER 설정, SSL_VERIFY_NONE 금지
  • TLS1_2_VERSION 이상
  • 강한 암호 스위트만 허용
  • CA 저장소 올바르게 로드

키·비밀 관리

  • 메모리에 최소 시간만 보관
  • 사용 후 OPENSSL_cleanse로 제로화
  • 로그에 키/비밀/토큰 출력 금지

빌드·테스트

  • -fsanitize=address,undefined로 디버그 빌드 테스트
  • 정적 분석기(Clang-Tidy, Coverity) 실행
  • Fuzzing(AFL, libFuzzer)으로 입력 검증 강화

9. 정리

항목요약
오버플로우정수·버퍼 검사·안전한 API·Sanitizer
OpenSSL에러 큐 확인·RAII 래퍼·초기화
암호화RAND_bytes·GCM·IV 재사용 금지·태그 검증
TLSPEER 검증·TLS 1.2+·강한 암호 스위트
키 관리최소 보관·제로화·로깅 금지

43-2로 보안 코딩OpenSSL 실전 연동 기초를 다뤘습니다. 문제 시나리오, 완전한 예제, 취약점, 모범 사례, 프로덕션 패턴을 적용하면 실무에서 안전한 C++ 암호화 코드를 작성할 수 있습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • C++ SSL/TLS 보안 통신 | OpenSSL과 Asio 연동 완벽 가이드 [#30-2]
  • Rust vs C++ 메모리 안전성 | 컴파일러 오류 차이 [#47-3]
  • C++ unique_ptr 고급 완벽 가이드 | 커스텀 삭제자·배열

실전 체크리스트

실무에서 이 개념을 적용할 때 확인해야 할 사항입니다.

코드 작성 전

  • 이 기법이 현재 문제를 해결하는 최선의 방법인가?
  • 팀원들이 이 코드를 이해하고 유지보수할 수 있는가?
  • 성능 요구사항을 만족하는가?

코드 작성 중

  • 컴파일러 경고를 모두 해결했는가?
  • 엣지 케이스를 고려했는가?
  • 에러 처리가 적절한가?

코드 리뷰 시

  • 코드의 의도가 명확한가?
  • 테스트 케이스가 충분한가?
  • 문서화가 되어 있는가?

이 체크리스트를 활용하여 실수를 줄이고 코드 품질을 높이세요.


이 글에서 다루는 키워드 (관련 검색어)

보안 코딩, OpenSSL, C++ 암호화, 오버플로우 방지, EVP API, AES-GCM, TLS 보안 등으로 검색하시면 이 글이 도움이 됩니다.

자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. 정수·버퍼 오버플로우 방지와 OpenSSL을 C++에서 안전하게 사용하는 패턴·에러 처리·리소스 관리를 다룹니다. API 서명 검증(HMAC), 데이터 암호화(AES-GCM), TLS 클라이언트/서버 등 실무 시나리오에 바로 적용할 수 있습니다.

Q. OpenSSL 대신 다른 라이브러리는?

A. BoringSSL(Google), libsodium(쉬운 API, NaCl 기반), mbedTLS(임베디드) 등이 있습니다. OpenSSL은 가장 널리 쓰이지만, API가 복잡하므로 프로젝트 요구에 맞는 선택이 필요합니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. OpenSSL 공식 문서, OWASP Cryptographic Storage Cheat Sheet를 참고하세요.

한 줄 요약: 오버플로우 방지·OpenSSL 연동으로 보안 코딩 기초를 다질 수 있습니다. 다음으로 Prometheus·Grafana(#43-3)를 읽어보면 좋습니다.

이전 글: 실전 도메인 #43-1: gRPC·Protobuf

다음 글: [실전 도메인 #43-3] Observability: Prometheus와 Grafana로 C++ 서버 모니터링 지표 추출


관련 글

  • C++ constexpr 완벽 가이드 | 컴파일 타임 계산·if constexpr·consteval 실전
  • C++ 고성능 RPC 시스템: gRPC와 Protocol Buffers를 이용한 마이크로서비스 구축
  • C++ constexpr 고급 가이드 | constexpr 컨테이너·알고리즘·문자열·new/delete 실전
  • C++ Observability: Prometheus와 Grafana로 C++ 서버 모니터링 구축하기
  • C++ 제약된 환경에서의 C++: Exception과 RTTI 없이 안전한 코드 짜기 [#42-1]