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 공격에 취약
이 글에서는 위와 같은 문제를 예방하는 패턴과 완전한 예제를 다룹니다.
목차
개념을 잡는 비유
소켓과 비동기 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_ptr에 Custom Deleter를 주는 방식이 간단합니다.
- 초기화: OpenSSL 3.x에서는 OPENSSL_init_ssl 등을 호출해야 합니다. thread safety는 기본으로 제공되지만, 옵션에 따라 다르므로 문서를 확인합니다.
EVP_PKEY_Deleter는 operator()(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 재사용 금지·태그 검증 |
| TLS | PEER 검증·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]