OpenSSL
핵심 요약
- OpenSSL 3.x는 Provider 기반 아키텍처로 전환되었으며, 암호화 구현을 모듈형 프로바이더(Provider)로 분리합니다.
- 커널 연동 4가지 채널 — AF_ALG(커널 Crypto API 호출), kTLS(TLS 데이터 경로 오프로드), /dev/urandom(난수 시드), 모듈 서명(sign-file)이 있습니다.
- FIPS 프로바이더는 FIPS 140-3 인증 환경에서 검증된 알고리즘만 허용하는 격리된 암호화 모듈입니다.
- 하드웨어 가속은 AES-NI, QAT 등을 Provider/ENGINE을 통해 자동 또는 명시적으로 활용합니다.
- 인증서 관리에서 OpenSSL은 키 생성, CSR 발급, 서명, 검증의 전체 생명주기를 담당합니다.
단계별 이해
- OpenSSL 계층 이해
Application → libssl(TLS 프로토콜) → libcrypto(암호 연산) → Provider(구현체) 구조를 파악합니다. - 커널 연동 포인트 파악
OpenSSL이 리눅스 커널과 상호작용하는 4가지 경로(AF_ALG, kTLS, /dev/urandom, 모듈 서명)를 이해합니다. - Provider 시스템 학습
Default, FIPS, Legacy, PKCS#11 프로바이더의 역할과 설정 방법을 익힙니다. - 보안 설정 적용
TLS 프로토콜 버전 제한, cipher suite 선택, openssl.cnf 강화 방법을 적용합니다. - 성능 최적화
하드웨어 가속 활용, 비동기 연산, openssl speed 벤치마크 해석으로 최적 구성을 찾습니다.
OpenSSL 3.x 아키텍처
OpenSSL 3.x는 1.x 시대의 모놀리식 구조를 탈피하여 프로바이더(Provider) 기반 모듈형 아키텍처로 전환되었습니다.
애플리케이션은 libssl(TLS 프로토콜 엔진)과 libcrypto(암호 연산 라이브러리)를 통해 암호화 기능을 사용하며,
실제 암호화 구현은 프로바이더라는 독립된 모듈이 담당합니다.
libssl vs libcrypto 역할 분리
| 계층 | 라이브러리 | 역할 | 주요 API |
|---|---|---|---|
| TLS 프로토콜 | libssl | TLS/DTLS 핸드셰이크, 레코드 처리, 세션 관리 | SSL_CTX_new(), SSL_read(), SSL_write() |
| 암호 연산 | libcrypto | 대칭/비대칭 암호, 해시, KDF, 난수 생성 | EVP_EncryptInit(), EVP_DigestSign(), EVP_PKEY_keygen() |
| 구현체 | Provider | 알고리즘의 실제 구현 (SW/HW) | OSSL_PROVIDER_load(), OSSL_FUNC_* |
| 하위 호환 | ENGINE (deprecated) | 1.x 호환 하드웨어 가속 인터페이스 | ENGINE_load_builtin_engines() |
ENGINE에서 Provider로의 전환
OpenSSL 1.x에서 하드웨어 가속이나 외부 키 저장소를 사용하려면 ENGINE API를 통해 커스텀 구현을 등록했습니다. OpenSSL 3.x에서 ENGINE은 deprecated 상태이며, 동일한 기능을 Provider가 대체합니다.
ENGINE의 구조적 문제점
ENGINE API가 deprecated된 것은 단순한 API 정리가 아니라, 근본적인 아키텍처 결함을 해결하기 위한 것입니다. OpenSSL 프로젝트가 공식적으로 밝힌 전환 이유와 기술적 배경을 분석합니다.
핵심 문제: ABI 경계의 부재
ENGINE의 가장 근본적인 문제는 libcrypto 내부 구조체를 직접 조작한다는 점입니다.
ENGINE은 RSA_METHOD, EC_KEY_METHOD, EVP_PKEY_METHOD 등의
내부 함수 포인터 테이블을 교체하는 방식으로 동작했습니다.
이로 인해 OpenSSL의 내부 구조가 변경되면 모든 ENGINE의 재컴파일이 필요했고,
마이너 버전 업데이트에서도 ABI 호환이 깨지는 경우가 빈번했습니다.
/* ENGINE의 ABI 문제 — 내부 구조체 직접 접근 (1.x) */
/* RSA_METHOD: libcrypto 내부 구조체 — 필드 순서/크기가 바뀌면 ABI 깨짐 */
static RSA_METHOD *my_rsa_method;
static int my_rsa_init(RSA *rsa) {
/* RSA 구조체 내부 필드에 직접 접근 — 버전별로 오프셋 다름 */
return 1;
}
static int my_rsa_sign(int type,
const unsigned char *m, unsigned int m_len,
unsigned char *sigret, unsigned int *siglen,
const RSA *rsa)
{
/* RSA 내부의 BIGNUM *n, *e, *d에 접근 — opaque화 후 깨짐 */
const BIGNUM *n, *e, *d;
RSA_get0_key(rsa, &n, &e, &d); /* 1.1.0에서 추가된 getter */
/* 1.0.x에서는 rsa->n, rsa->d 직접 접근 — 1.1에서 깨짐! */
}
void engine_init(void) {
my_rsa_method = RSA_meth_new("my_rsa", 0);
RSA_meth_set_sign(my_rsa_method, my_rsa_sign);
RSA_meth_set_init(my_rsa_method, my_rsa_init);
ENGINE *e = ENGINE_new();
ENGINE_set_id(e, "my_engine");
ENGINE_set_RSA(e, my_rsa_method); /* RSA 전체를 교체 */
ENGINE_add(e); /* 글로벌 테이블에 등록 — 프로세스 전체 영향 */
}
/* ❌ 문제점:
* 1. RSA_METHOD 구조체가 OpenSSL 버전마다 다를 수 있음
* 2. ENGINE_add()는 글로벌 상태 변경 → 모든 스레드에 영향
* 3. RSA 전체를 교체 — 일부 연산만 가속 불가
* 4. FIPS 모듈은 이 ENGINE을 인식하지 못함
*/
/* Provider의 ABI 해결 — OSSL_DISPATCH 함수 테이블 (3.x) */
/* Provider는 번호(function_id)로 함수를 등록 — 구조체 레이아웃 무관 */
static const OSSL_DISPATCH my_rsa_sign_funcs[] = {
{ OSSL_FUNC_SIGNATURE_NEWCTX,
(void (*)())my_rsa_sign_newctx },
{ OSSL_FUNC_SIGNATURE_SIGN_INIT,
(void (*)())my_rsa_sign_init },
{ OSSL_FUNC_SIGNATURE_SIGN,
(void (*)())my_rsa_sign },
{ OSSL_FUNC_SIGNATURE_FREECTX,
(void (*)())my_rsa_sign_freectx },
{ 0, NULL }
};
/* ✓ 장점:
* 1. function_id는 안정 — OpenSSL 버전 간 ABI 호환
* 2. Provider 자체 컨텍스트 사용 — libcrypto 내부 접근 불필요
* 3. 서명 연산만 교체 가능 — 나머지는 Default Provider가 처리
* 4. OSSL_PARAM으로 파라미터 전달 — 구조체 의존성 제거
*/
글로벌 상태 문제와 멀티테넌시
ENGINE은 프로세스당 하나의 글로벌 테이블에 등록되었습니다.
ENGINE_set_default(e, ENGINE_METHOD_RSA)를 호출하면
프로세스 내 모든 RSA 연산이 해당 ENGINE을 사용하게 됩니다.
클라우드 환경에서 테넌트별로 다른 HSM을 사용하거나,
일부 요청만 하드웨어 가속을 적용하는 것이 불가능했습니다.
| 시나리오 | ENGINE (1.x) | Provider (3.x) |
|---|---|---|
| 테넌트 A: HSM 사용 | 프로세스 전체가 HSM ENGINE 사용 | OSSL_LIB_CTX_A에 PKCS#11 Provider 로드 |
| 테넌트 B: SW만 | HSM ENGINE 사용 강제 (분리 불가) | OSSL_LIB_CTX_B에 Default Provider만 로드 |
| FIPS + 비FIPS 혼용 | 불가 (ENGINE은 FIPS 경계 밖) | FIPS 컨텍스트와 Default 컨텍스트 병존 |
| 알고리즘 선택 제어 | ENGINE ID로 명시적 지정만 | property query로 자동 매칭 |
FIPS 인증과 ENGINE의 비양립성
FIPS 140 인증에서 암호화 모듈 경계(Cryptographic Module Boundary)는 핵심 개념입니다. 경계 안의 모든 코드는 검증 대상이며, 경계 밖의 코드가 경계 안의 상태를 변경할 수 없어야 합니다. ENGINE은 libcrypto 내부에서 동작하면서 외부 코드(ENGINE .so)가 암호 연산을 대체하므로, FIPS 모듈 경계를 정의할 수 없었습니다. Provider 아키텍처에서는 FIPS Provider 자체가 독립된 모듈 경계를 형성하여, 자체 무결성 검증(HMAC-SHA256)을 통과한 후에만 암호 연산을 수행합니다.
ENGINE vs Provider 상세 비교
| 항목 | ENGINE (1.x) | Provider (3.x) |
|---|---|---|
| 등록 방식 | ENGINE_add() → 글로벌 테이블 | OSSL_PROVIDER_load(ctx, name) → 컨텍스트별 |
| 설정 파일 | openssl.cnf [engine_section] | openssl.cnf [provider_sect] |
| 알고리즘 탐색 | ENGINE ID 기반 명시적 선택 | property query 기반 자동 선택 |
| ABI 경계 | 없음 (RSA_METHOD 등 내부 구조체) | OSSL_DISPATCH 함수 테이블 (안정) |
| FIPS 지원 | 불가 (모듈 경계 정의 불가) | FIPS Provider = 독립 모듈 경계 |
| 멀티 인스턴스 | 글로벌 싱글턴 | OSSL_LIB_CTX 별 독립 인스턴스 |
| 교체 단위 | 알고리즘 카테고리 전체 (RSA 전부) | 개별 알고리즘/연산 단위 |
| 파라미터 전달 | ctrl() 함수, 구조체별 다름 | OSSL_PARAM 통일 인터페이스 |
| 메모리 관리 | ENGINE 자체 할당기 → 불일치 가능 | CORE 제공 할당 함수 사용 |
| 에러 전파 | 불투명 (ENGINE 내부 에러 유실) | CORE 에러 시스템 통합 |
| 로딩 메커니즘 | dlopen() + ENGINE_by_id() | dlopen() + OSSL_provider_init() |
| 상태 | Deprecated (3.x), 제거 예정 (4.x) | 현재 권장, 향후 유일 방식 |
ENGINE → Provider 마이그레이션 코드
/* ❌ ENGINE 방식 (deprecated) — PKCS#11 HSM 연동 */
ENGINE_load_builtin_engines();
ENGINE *e = ENGINE_by_id("pkcs11");
if (!e) { /* 에러 */ }
ENGINE_ctrl_cmd_string(e, "MODULE_PATH",
"/usr/lib64/softhsm/libsofthsm2.so", 0);
ENGINE_init(e);
ENGINE_set_default(e, ENGINE_METHOD_ALL); /* 전체 교체! */
/* 키 로드 — ENGINE 전용 API */
EVP_PKEY *pkey = ENGINE_load_private_key(e,
"pkcs11:token=mytoken;object=mykey", NULL, NULL);
ENGINE_finish(e);
ENGINE_free(e);
/* ✓ Provider 방식 (권장) — PKCS#11 HSM 연동 */
OSSL_LIB_CTX *ctx = OSSL_LIB_CTX_new(); /* 격리된 컨텍스트 */
OSSL_PROVIDER *prov = OSSL_PROVIDER_load(ctx, "pkcs11");
OSSL_PROVIDER *base = OSSL_PROVIDER_load(ctx, "base");
if (!prov) { ERR_print_errors_fp(stderr); }
/* 키 로드 — 표준 EVP API (Provider가 자동 처리) */
OSSL_STORE_CTX *sctx = OSSL_STORE_open_ex(
"pkcs11:token=mytoken;object=mykey",
ctx, NULL, NULL, NULL, NULL, NULL, NULL);
OSSL_STORE_INFO *info = OSSL_STORE_load(sctx);
EVP_PKEY *pkey = OSSL_STORE_INFO_get1_PKEY(info);
OSSL_STORE_close(sctx);
/* Provider는 컨텍스트 해제 시 자동 언로드 */
OSSL_LIB_CTX_free(ctx);
no-engine 빌드 옵션으로 컴파일 시 즉시 감지할 수 있습니다.
OpenSSL 소스 코드 구조
OpenSSL 3.x의 소스 코드는 기능별로 명확하게 분리된 디렉터리 구조를 갖습니다.
Provider 아키텍처 도입 이후 providers/ 디렉터리가 핵심 확장점이 되었으며,
기존 crypto/는 libcrypto의 내부 구현을, ssl/은 TLS 프로토콜 엔진을 담당합니다.
| 디렉터리 | 역할 | 주요 파일 |
|---|---|---|
ssl/ | libssl — TLS/DTLS 프로토콜 구현 | ssl_lib.c, statem/, record/, quic/ |
crypto/ | libcrypto — 암호 알고리즘 내부 구현 | evp/, aes/, rsa/, ec/, rand/ |
providers/ | Provider 모듈 (default, fips, legacy) | implementations/, common/, fips/ |
include/openssl/ | 공개 API 헤더 | ssl.h, evp.h, bio.h, x509.h |
include/internal/ | 내부 전용 헤더 (Provider 간 공유) | provider.h, core.h |
apps/ | CLI 도구 (openssl 명령) | s_client.c, speed.c, req.c |
engines/ | ENGINE 모듈 (deprecated) | e_afalg.c (AF_ALG ENGINE) |
test/ | 테스트 스위트 | evp_test.c, ssl_test.c |
crypto/rand/ | 난수 생성기 (DRBG) | rand_lib.c, rand_pool.c |
ssl/statem/ | TLS 상태 머신 | statem.c, statem_clnt.c, statem_srvr.c |
빌드 시스템 개요
OpenSSL은 Perl 기반 Configure 스크립트를 사용하며,
빌드 옵션에 따라 포함되는 알고리즘, Provider, 최적화 수준이 달라집니다.
빌드 시스템은 Configure(Perl) → Makefile(생성) → make 순으로 동작합니다.
# 기본 빌드 흐름
./Configure linux-x86_64 --prefix=/usr/local/openssl \
--openssldir=/etc/ssl shared -O2
make -j$(nproc) # 병렬 빌드
make test # 테스트 (~5분, ~250개 테스트)
make install # 설치
make install_fips # FIPS 모듈만 별도 설치
make install_ssldirs # openssldir 디렉터리만 설치
# 빌드 설정 확인
openssl version -a
# OpenSSL 3.3.0 ...
# compiler: gcc -fPIC -O2 -DOPENSSL_USE_NODELETE ...
# OPENSSLDIR: "/etc/ssl"
# MODULESDIR: "/usr/local/openssl/lib64/ossl-modules"
# 사용 가능한 플랫폼 타겟 조회
./Configure LIST
# linux-x86_64, linux-aarch64, linux-mips64, ...
# darwin64-x86_64-cc, darwin64-arm64-cc, ...
# mingw64, VC-WIN64A, ...
# 현재 설정으로 활성화된 옵션 확인
perl configdata.pm --dump
경로 및 설치 옵션
| 옵션 | 기본값 | 설명 |
|---|---|---|
--prefix=DIR | /usr/local | 설치 기본 경로 (bin/, lib/, include/) |
--openssldir=DIR | /usr/local/ssl | 설정 파일 경로 (openssl.cnf, certs/, private/) |
--libdir=DIR | lib 또는 lib64 | 라이브러리 설치 디렉터리명 |
--banner=TEXT | — | openssl version 출력에 추가 텍스트 |
--with-rand-seed=X | 자동 감지 | 난수 시드 소스 (os, devrandom, egd, rdcpu, librandom 등) |
--cross-compile-prefix=X | — | 크로스 컴파일 접두사 (예: aarch64-linux-gnu-) |
--api=X | — | 호환 API 레벨 (1.0.0, 1.1.0, 3.0 등) — 이전 API 유지 |
라이브러리 빌드 옵션
| 옵션 | 효과 | 사용 상황 |
|---|---|---|
shared | 공유 라이브러리(.so/.dylib) 빌드 | 대부분의 서버/배포 환경 |
no-shared | 정적 라이브러리(.a)만 빌드 | 정적 링크 바이너리, 컨테이너 |
no-pic | PIC(Position Independent Code) 비활성화 | 정적 전용 빌드 시 성능 최적화 |
no-asm | 어셈블리 최적화 비활성화 (순수 C) | 미지원 아키텍처, 디버깅 |
no-apps | openssl CLI 도구 빌드 제외 | 라이브러리만 필요한 임베디드 |
no-docs | man 페이지 빌드/설치 제외 | 최소 설치 |
no-tests | 테스트 바이너리 빌드 제외 | CI에서 빌드만 검증 시 |
no-module | 동적 Provider .so 빌드 비활성화 | 정적 Provider만 사용 |
프로토콜 옵션
| 옵션 | 효과 | 보안/성능 영향 |
|---|---|---|
no-ssl3 | SSLv3 비활성화 | POODLE 공격 방지 (기본 비활성화) |
no-tls1 | TLS 1.0 비활성화 | BEAST, Lucky13 방지 |
no-tls1_1 | TLS 1.1 비활성화 | 약한 cipher 제거 |
no-tls1_2 | TLS 1.2 비활성화 | TLS 1.3 전용 환경 |
no-tls1_3 | TLS 1.3 비활성화 | 레거시 호환 필요 시만 |
no-dtls | DTLS 전체 비활성화 | UDP 기반 TLS 불필요 시 |
no-dtls1 | DTLS 1.0 비활성화 | 보안 강화 |
no-dtls1_2 | DTLS 1.2 비활성화 | — |
enable-ktls | kTLS 오프로드 활성화 | Linux 4.13+에서 성능 향상 |
enable-quic | QUIC 프로토콜 API 활성화 | HTTP/3 지원 (3.2+) |
no-nextprotoneg | NPN 확장 비활성화 | ALPN만 사용 (NPN은 deprecated) |
no-srp | SRP(Secure Remote Password) 비활성화 | 사용처 거의 없음 |
no-srtp | SRTP(Secure RTP) 확장 비활성화 | WebRTC 미사용 시 |
no-psk | PSK(Pre-Shared Key) TLS 비활성화 | PSK 미사용 시 |
no-comp | TLS 압축 비활성화 | CRIME 공격 방지 |
no-ocsp | OCSP 지원 비활성화 | CRL만 사용 시 |
no-ct | Certificate Transparency 비활성화 | CT 검증 불필요 시 |
no-cms | CMS(Cryptographic Message Syntax) 비활성화 | S/MIME 불필요 시 |
no-ts | 타임스탬프 프로토콜(TSP) 비활성화 | TSA 불필요 시 |
알고리즘 옵션
| 옵션 | 비활성화 대상 | 보안 영향 |
|---|---|---|
| 대칭 암호 | ||
no-des | DES, 3DES | Sweet32 공격 방지 (64비트 블록) |
no-rc2 | RC2 | 취약 알고리즘 제거 |
no-rc4 | RC4 | RC4 바이어스 공격 방지 |
no-rc5 | RC5 | 특허 만료, 사용처 없음 |
no-idea | IDEA | 사용처 거의 없음 |
no-seed | SEED (한국 표준) | 한국 외 사용처 없음 |
no-bf | Blowfish | 64비트 블록, 레거시 |
no-cast | CAST5 | 사용처 거의 없음 |
no-camellia | Camellia | 일본 표준, AES 대안 |
no-aria | ARIA (한국 표준) | 한국 외 사용처 제한적 |
no-sm4 | SM4 (중국 표준) | 중국 외 사용처 없음 |
no-chacha | ChaCha20-Poly1305 | 모바일 환경에서 AES-NI 없을 때 유용 |
| 해시 | ||
no-md2 | MD2 | 완전히 깨진 해시 (기본 비활성화) |
no-md4 | MD4 | 완전히 깨진 해시 |
no-md5 | MD5 | 충돌 공격 가능 (HMAC은 안전) |
no-rmd160 | RIPEMD-160 | 비트코인 외 사용처 제한적 |
no-whirlpool | Whirlpool | 사용처 거의 없음 |
no-sm3 | SM3 (중국 표준) | 중국 외 사용처 없음 |
| 비대칭 | ||
no-dsa | DSA | EdDSA/ECDSA로 대체 |
no-dh | DH(Diffie-Hellman) | ECDH로 대체 가능 |
no-ec | 타원 곡선 전체 | ECDSA/ECDH/Ed25519 모두 비활성화 |
no-ec2m | GF(2^m) 타원 곡선 | 바이너리 곡선 제거 (GF(p)만 유지) |
no-sm2 | SM2 (중국 표준) | 중국 외 사용처 없음 |
| 기타 | ||
no-gost | GOST (러시아 표준) | 러시아 외 사용처 없음 |
no-siv | AES-SIV (RFC 5297) | nonce 오용 방지 모드 |
no-poly1305 | Poly1305 MAC | ChaCha20-Poly1305에 필요 |
Provider 및 ENGINE 옵션
| 옵션 | 효과 | 사용 상황 |
|---|---|---|
enable-fips | FIPS Provider 빌드 | FIPS 140-3 규제 환경 (금융, 정부) |
no-legacy | Legacy Provider 빌드 제외 | 보안 강화 (취약 알고리즘 완전 제거) |
no-engine | ENGINE 서브시스템 전체 제거 | Provider만 사용, ENGINE 의존성 감지 |
no-deprecated | deprecated API 컴파일 제외 | 새 프로젝트, 1.x 코드 미사용 확인 |
no-module | 동적 Provider .so 빌드 비활성화 | 정적 Provider 내장 |
no-autoload-config | openssl.cnf 자동 로딩 비활성화 | Provider를 코드로만 로드 |
보안 및 디버그 옵션
| 옵션 | 효과 | 사용 상황 |
|---|---|---|
--debug | 디버그 심볼 + 최적화 해제 (-g -O0) | 개발/디버깅 |
--release | 릴리스 최적화 (-O3) | 프로덕션 빌드 |
enable-asan | AddressSanitizer 활성화 | 힙/스택 오버플로 탐지 |
enable-ubsan | UndefinedBehaviorSanitizer | 정의되지 않은 동작 탐지 |
enable-msan | MemorySanitizer (Clang 전용) | 초기화되지 않은 메모리 읽기 탐지 |
enable-tsan | ThreadSanitizer | 데이터 레이스 탐지 |
enable-fuzz-libfuzzer | libFuzzer 퍼징 빌드 | 보안 테스팅 |
enable-fuzz-afl | AFL 퍼징 빌드 | 보안 테스팅 |
no-secure-memory | mlock() 기반 보안 메모리 비활성화 | mlock 권한 없는 환경 |
-DPURIFY | Valgrind 호환 모드 | 메모리 분석 도구 사용 시 |
no-threads | 멀티스레딩 지원 비활성화 | 단일 스레드 임베디드 |
no-async | 비동기 연산 비활성화 | QAT 등 비동기 Provider 미사용 |
플랫폼 및 최적화 옵션
| 옵션 | 효과 | 사용 상황 |
|---|---|---|
no-asm | 어셈블리 최적화 비활성화 (순수 C) | 미지원 아키텍처, 디버깅, ASAN |
386 | x86 386 호환 코드 생성 | 레거시 x86 (SSE2 미지원) |
no-sse2 | SSE2 사용 비활성화 | 매우 오래된 x86 |
enable-ec_nistp_64_gcc_128 | P-256/P-521 고속 구현 (GCC __int128) | x86_64 GCC 빌드 성능 향상 |
no-hw | 하드웨어 가속 비활성화 | 순수 SW 테스트, FIPS 기준선 |
no-afalgeng | AF_ALG ENGINE 비활성화 | AF_ALG 미사용 |
no-padlockeng | VIA PadLock ENGINE 비활성화 | VIA CPU 미사용 |
enable-zlib | zlib 기반 압축 활성화 | CMS/PKCS7 압축 (TLS 압축은 별도) |
enable-brotli | Brotli 인증서 압축 (RFC 8879) | TLS 인증서 크기 절감 |
enable-zstd | Zstandard 인증서 압축 (RFC 8879) | TLS 인증서 크기 절감 |
실전 빌드 레시피
# 1. 프로덕션 서버 (TLS 1.2/1.3, 보안 강화)
./Configure linux-x86_64 \
--prefix=/usr/local/openssl --openssldir=/etc/ssl \
--release shared enable-ktls \
no-ssl3 no-tls1 no-tls1_1 \
no-des no-rc4 no-rc2 no-idea no-seed no-bf no-cast \
no-md5 no-whirlpool \
no-comp no-engine no-legacy \
no-srp no-psk \
enable-ec_nistp_64_gcc_128
# 2. FIPS 규제 환경
./Configure linux-x86_64 \
--prefix=/opt/openssl-fips --openssldir=/etc/ssl-fips \
shared enable-fips \
no-legacy no-engine \
no-comp no-srp
# 3. 임베디드 최소 빌드 (ARM64)
./Configure linux-aarch64 \
--cross-compile-prefix=aarch64-linux-gnu- \
--prefix=/opt/openssl-arm64 \
no-shared no-apps no-docs no-tests \
no-deprecated no-engine no-legacy \
no-des no-rc4 no-rc2 no-idea no-seed no-bf no-cast \
no-md5 no-whirlpool no-rmd160 \
no-dsa no-ec2m no-gost no-sm2 no-sm3 no-sm4 \
no-dtls no-srp no-psk no-srtp \
no-comp no-ocsp no-ct no-cms no-ts \
no-async no-stdio
# 4. 보안 테스팅 (퍼징 + Sanitizer)
CC=clang ./Configure linux-x86_64 \
--debug enable-asan enable-ubsan enable-fuzz-libfuzzer \
no-shared no-module -DPURIFY
# 5. Valgrind 분석 빌드
./Configure linux-x86_64 \
--debug no-asm no-shared -DPURIFY
# 6. HTTP/3 (QUIC) 지원 빌드
./Configure linux-x86_64 \
--prefix=/opt/openssl-quic \
shared enable-quic enable-ktls
# 7. 크기 최적화 빌드 (IoT)
./Configure linux-armv4 \
--cross-compile-prefix=arm-linux-gnueabihf- \
no-shared no-asm no-apps no-docs no-tests \
no-deprecated no-engine no-legacy no-module \
no-des no-rc4 no-rc2 no-idea no-seed no-bf no-cast \
no-md5 no-whirlpool no-rmd160 no-md4 \
no-dsa no-dh no-ec2m no-gost no-sm2 no-sm3 no-sm4 \
no-dtls no-srp no-psk no-srtp no-ocsp no-ct no-cms no-ts \
no-comp no-async no-stdio no-camellia no-aria \
-Os
libcrypto.so는 약 4~5MB, libssl.so는 약 700KB입니다.
최소 빌드(IoT 레시피)로 정적 링크하면 libcrypto.a를 ~1.5MB까지 줄일 수 있습니다.
strip 후 최소 바이너리는 약 800KB~1MB 수준입니다.
BIO 추상화 계층
BIO(Basic I/O)는 OpenSSL의 I/O 추상화 계층으로, 소켓, 파일, 메모리 버퍼, SSL 연결 등 다양한 데이터 소스/싱크를 동일한 인터페이스로 다룹니다. BIO는 필터 체인(Filter Chain)으로 연결할 수 있어, 데이터가 여러 변환 단계를 거치도록 구성할 수 있습니다.
/* BIO 체인 예제: 버퍼링 + Base64 + 파일 출력 */
BIO *bio_file = BIO_new_file("output.b64", "w");
BIO *bio_b64 = BIO_new(BIO_f_base64());
BIO *bio_buf = BIO_new(BIO_f_buffer());
/* 체인 연결: buf → b64 → file */
BIO_push(bio_buf, bio_b64);
BIO_push(bio_b64, bio_file);
/* 쓰기: 데이터 → 버퍼링 → Base64 인코딩 → 파일 */
BIO_write(bio_buf, data, data_len);
BIO_flush(bio_buf);
/* 체인 전체 해제 */
BIO_free_all(bio_buf);
/* BIO + SSL 연결 예제: TLS 클라이언트 */
SSL_CTX *ctx = SSL_CTX_new(TLS_client_method());
SSL_CTX_set_default_verify_paths(ctx);
/* BIO_new_ssl_connect: socket + ssl 필터 자동 구성 */
BIO *bio = BIO_new_ssl_connect(ctx);
SSL *ssl;
BIO_get_ssl(bio, &ssl);
SSL_set_mode(ssl, SSL_MODE_AUTO_RETRY);
SSL_set1_host(ssl, "example.com");
/* 연결 */
BIO_set_conn_hostname(bio, "example.com:443");
if (BIO_do_connect(bio) <= 0) {
ERR_print_errors_fp(stderr);
return 1;
}
/* HTTP 요청 */
BIO_puts(bio, "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n");
char buf[4096];
int len = BIO_read(bio, buf, sizeof(buf) - 1);
buf[len] = '\0';
printf("%s", buf);
BIO_free_all(bio);
SSL_CTX_free(ctx);
OpenSSL Provider 상세
Provider는 OpenSSL 3.x의 핵심 확장 메커니즘입니다.
각 Provider는 독립된 공유 라이브러리(.so/.dll)로 존재하며,
openssl.cnf의 [provider_sect] 설정이나 API 호출로 로딩됩니다.
Default 프로바이더
Default 프로바이더는 별도 설정 없이 자동 로딩되며, OpenSSL의 모든 범용 알고리즘을 소프트웨어로 구현합니다. CPU가 AES-NI나 SHA 확장을 지원하면 자동으로 하드웨어 가속 코드 경로를 사용합니다.
| 카테고리 | 포함 알고리즘 |
|---|---|
| 대칭 암호 | AES-128/192/256 (CBC, CTR, GCM, CCM, XTS, WRAP), ChaCha20-Poly1305, ARIA, Camellia, SM4 |
| 해시 | SHA-1, SHA-224/256/384/512, SHA3-*, SHAKE128/256, BLAKE2b/2s, SM3 |
| MAC | HMAC, CMAC, GMAC, Poly1305, KMAC |
| 비대칭 | RSA (PKCS#1, PSS, OAEP), EC (NIST P-256/384/521, secp256k1), Ed25519/Ed448, X25519/X448, SM2 |
| KDF | HKDF, PBKDF2, scrypt, SSHKDF, TLS13-KDF, SSKDF, X963KDF |
| 난수 | CTR-DRBG, HASH-DRBG, HMAC-DRBG (커널 /dev/urandom으로 시드) |
FIPS 프로바이더
FIPS 프로바이더는 FIPS 140-3에서 요구하는 암호화 모듈 경계(Cryptographic Module Boundary)를 구현합니다. 로딩 시 자체 무결성 검증(HMAC-SHA256)을 수행하고, 검증에 실패하면 모든 암호화 연산을 거부합니다.
# /etc/ssl/openssl.cnf — FIPS 프로바이더 설정
[openssl_init]
providers = provider_sect
[provider_sect]
fips = fips_sect
base = base_sect
[base_sect]
activate = 1
[fips_sect]
activate = 1
module-mac = B8:2A:... # fipsmodule.cnf에서 자동 생성된 MAC
# FIPS 프로바이더 설치 및 설정
openssl fipsinstall -out /etc/ssl/fipsmodule.cnf -module /usr/lib64/ossl-modules/fips.so
# FIPS 모드 검증
openssl list -providers
# 출력에 "name: OpenSSL FIPS Provider" 포함 확인
# FIPS 모드에서 허용되지 않는 알고리즘 테스트
openssl enc -des-cbc -provider fips -in test.txt -out test.enc
# Error: unsupported algorithm (DES는 FIPS에서 금지)
enable-fips)가 필요하며, 배포판에 따라 패키지가 분리되어 있습니다.
FIPS 모드에서는 MD5, DES, RC4, Blowfish 등 취약한 알고리즘이 모두 비활성화됩니다.
Legacy 프로바이더
Legacy 프로바이더는 보안상 권장되지 않지만 호환성을 위해 유지되는 알고리즘을 제공합니다. Default 프로바이더에서 제거된 MD2, MD4, MDC2, Whirlpool, DES, RC2, RC4, RC5, SEED, Blowfish, CAST5 등이 포함됩니다.
# Legacy 프로바이더 명시적 로딩 (필요한 경우만)
openssl dgst -md4 -provider legacy -provider default file.bin
# openssl.cnf로 설정
# [provider_sect]
# legacy = legacy_sect
# [legacy_sect]
# activate = 1
PKCS#11 프로바이더 (HSM 연동)
PKCS#11 프로바이더는 HSM(Hardware Security Module)이나 스마트카드에 저장된 키를 OpenSSL에서 사용할 수 있게 합니다.
OpenSSL 3.x에서는 pkcs11-provider 프로젝트를 통해 Provider 인터페이스로 HSM과 연동합니다.
# openssl.cnf — PKCS#11 Provider 설정 (OpenSSL 3.x)
[provider_sect]
pkcs11 = pkcs11_sect
default = default_sect
[default_sect]
activate = 1
[pkcs11_sect]
module = /usr/lib64/ossl-modules/pkcs11.so
pkcs11-module-path = /usr/lib64/softhsm/libsofthsm2.so
activate = 1
# PKCS#11 Provider를 통한 RSA 키 생성 (HSM 내부)
openssl genpkey -provider pkcs11 -provider default \
-algorithm RSA -pkeyopt rsa_keygen_bits:2048 \
-pkeyopt pkcs11_uri:"pkcs11:token=mytoken;object=mykey"
# HSM 키로 CSR 생성
openssl req -new -provider pkcs11 -provider default \
-key "pkcs11:token=mytoken;object=mykey" \
-out server.csr -subj "/CN=example.com"
# HSM 키로 인증서 서명
openssl x509 -req -in server.csr \
-provider pkcs11 -provider default \
-CAkey "pkcs11:token=CA;object=ca-key" \
-CA ca.crt -CAcreateserial -out server.crt
OSSL_LIB_CTX 멀티 컨텍스트 격리
OpenSSL 3.x는 OSSL_LIB_CTX를 통해 라이브러리 전역 상태를 컨텍스트별로 격리할 수 있습니다. 이는 동일 프로세스 내에서 FIPS Provider와 Default Provider를 독립적으로 운용하거나, 멀티테넌트 애플리케이션에서 테넌트별 암호화 설정을 분리하는 데 사용됩니다.
/* FIPS 전용 컨텍스트 생성 */
OSSL_LIB_CTX *fips_ctx = OSSL_LIB_CTX_new();
/* FIPS Provider만 로드 */
OSSL_PROVIDER *fips = OSSL_PROVIDER_load(fips_ctx, "fips");
OSSL_PROVIDER *base = OSSL_PROVIDER_load(fips_ctx, "base");
/* FIPS 컨텍스트에서 SHA-256 fetch */
EVP_MD *md = EVP_MD_fetch(fips_ctx, "SHA2-256", NULL);
/* → FIPS Provider의 검증된 구현이 선택됨 */
/* 기본 컨텍스트(NULL)에서는 Default Provider 사용 */
EVP_MD *md_default = EVP_MD_fetch(NULL, "SHA2-256", NULL);
/* → Default Provider의 범용 구현이 선택됨 */
/* 정리 */
EVP_MD_free(md);
EVP_MD_free(md_default);
OSSL_PROVIDER_unload(fips);
OSSL_PROVIDER_unload(base);
OSSL_LIB_CTX_free(fips_ctx);
OpenSSL과 커널 연동 포인트
OpenSSL은 순수 사용자 공간(User Space) 라이브러리이지만, 리눅스 커널과 4가지 핵심 채널을 통해 상호작용합니다. 이 연동을 이해하면 성능 최적화와 보안 강화의 핵심 의사결정을 내릴 수 있습니다.
AF_ALG — 커널 Crypto API 호출
AF_ALG은 사용자 공간 프로그램이 소켓 인터페이스를 통해 커널의 Crypto API를 호출할 수 있게 하는 주소 체계입니다.
OpenSSL은 afalg ENGINE(1.x) 또는 Provider(3.x)를 통해 AF_ALG를 사용할 수 있습니다.
| 항목 | OpenSSL SW 구현 | AF_ALG (커널 Crypto API) |
|---|---|---|
| 실행 위치 | 사용자 공간 (libcrypto) | 커널 공간 (af_alg.ko) |
| 컨텍스트 전환 | 없음 | send()/recv() 마다 발생 |
| HW 가속 활용 | AES-NI 직접 사용 (SIMD) | 커널 등록 HW 드라이버 사용 |
| 적합한 상황 | 범용 서버, 대부분의 경우 | 전용 HW 가속기 (QAT 등) 보유 시 |
| 성능 (AES-256-GCM, 8KB) | ~1.5 GB/s | ~1.2 GB/s (syscall 오버헤드) |
| splice() 제로카피 | 불가 | 지원 (대용량 파일 암호화) |
/* AF_ALG을 직접 사용하는 예제 — AES-256-CBC 암호화 */
#include <linux/if_alg.h>
#include <sys/socket.h>
struct sockaddr_alg sa = {
.salg_family = AF_ALG,
.salg_type = "skcipher",
.salg_name = "cbc(aes)",
};
int tfmfd = socket(AF_ALG, SOCK_SEQPACKET, 0);
bind(tfmfd, (struct sockaddr *)&sa, sizeof(sa));
setsockopt(tfmfd, SOL_ALG, ALG_SET_KEY, key, 32);
int opfd = accept(tfmfd, NULL, 0);
/* cmsg로 IV와 ALG_OP_ENCRYPT 설정 후 sendmsg/read */
splice() 제로 카피와 결합하면 유리합니다.
자세한 비교는 Crypto API의 AF_ALG 섹션을 참고하세요.
/dev/urandom과 getrandom() — 난수 시드
OpenSSL의 DRBG(Deterministic Random Bit Generator)는 커널의 난수 소스로 시드(Seed)를 공급받습니다.
OpenSSL 3.x는 기본적으로 getrandom(2) 시스템 호출을 사용하며,
이는 커널의 ChaCha20 기반 CRNG에서 암호학적으로 안전한 난수를 획득합니다.
| 인터페이스 | 블로킹 | 엔트로피 보장 | OpenSSL 사용 방식 |
|---|---|---|---|
getrandom(buf, len, 0) | 초기 시드 전까지만 | 예 (CRNG 초기화 후) | 기본 (OpenSSL 3.x) |
/dev/urandom | 비블로킹 | 초기 부팅 시 미보장 | 폴백 (getrandom 미지원 시) |
/dev/random | 블로킹 가능 | 항상 보장 | 사용 안 함 |
RDRAND (x86) | 비블로킹 | 하드웨어 의존 | 추가 엔트로피 소스 |
/* OpenSSL 내부 — DRBG 시드 획득 흐름 (간략화) */
/* crypto/rand/rand_lib.c */
static int rand_pool_acquire_entropy(RAND_POOL *pool)
{
unsigned char buf[256];
/* 1. getrandom() 시스콜 시도 */
ssize_t n = syscall(SYS_getrandom, buf, sizeof(buf), GRND_NONBLOCK);
if (n > 0) {
rand_pool_add(pool, buf, n, n * 8);
return 1;
}
/* 2. /dev/urandom 폴백 */
int fd = open("/dev/urandom", O_RDONLY);
if (fd >= 0) {
read(fd, buf, sizeof(buf));
rand_pool_add(pool, buf, sizeof(buf), sizeof(buf) * 8);
close(fd);
return 1;
}
return 0;
}
kTLS — 커널 TLS 오프로드
kTLS(Kernel TLS)는 TLS 핸드셰이크는 사용자 공간(OpenSSL)에서 처리하고, 핸드셰이크 완료 후 대칭키와 시퀀스 번호를 커널에 전달하여 데이터 경로(record layer)를 커널이 직접 처리하는 기술입니다. 이를 통해 사용자 공간 ↔ 커널 공간 데이터 복사를 줄이고, NIC의 TLS 하드웨어 오프로드까지 연결할 수 있습니다.
/* OpenSSL에서 kTLS 활성화 — SSL_CTX 설정 */
SSL_CTX *ctx = SSL_CTX_new(TLS_server_method());
/* kTLS 활성화 (커널 tls.ko 모듈 필요) */
SSL_CTX_set_options(ctx, SSL_OP_ENABLE_KTLS);
/* 핸드셰이크 완료 후, OpenSSL이 자동으로:
* 1. setsockopt(fd, SOL_TLS, TLS_TX, ...) 호출
* 2. 대칭키, IV, 시퀀스 번호를 커널에 전달
* 3. 이후 SSL_write()는 커널 kTLS가 암호화 처리
*/
/* sendfile()로 제로 카피 TLS 전송 (kTLS 필수) */
SSL_sendfile(ssl, fd, offset, size, 0);
/* 파일 → 커널 → NIC 직접 전송 (사용자 공간 복사 없음) */
# kTLS 모듈 로드
modprobe tls
# kTLS 동작 확인
cat /proc/net/tls_stat
# TlsCurrTxSw 4 ← SW kTLS 세션 수
# TlsCurrRxSw 4
# TlsTxDevice 0 ← HW 오프로드 세션 수
# TlsRxDevice 0
# nginx에서 kTLS 활성화
# nginx.conf:
# ssl_conf_command Options KTLS;
커널 모듈 서명
리눅스 커널의 모듈 서명(Module Signing) 기능은 OpenSSL을 사용하여 서명용 키 쌍을 생성합니다.
커널 빌드 시 scripts/sign-file 유틸리티가 OpenSSL의 libcrypto를 링크하여
모듈(.ko)에 PKCS#7 서명을 추가합니다.
# 커널 빌드 시 자동 생성되는 서명 키 (openssl 사용)
# certs/signing_key.pem, certs/signing_key.x509
# 수동으로 서명 키 생성
openssl req -new -nodes -utf8 -sha512 -days 36500 \
-batch -x509 -config x509.genkey \
-outform PEM -out signing_key.pem \
-keyout signing_key.pem
# 모듈 서명
scripts/sign-file sha512 certs/signing_key.pem \
certs/signing_key.x509 module.ko
# 서명 확인
modinfo module.ko | grep sig
# sig_id: PKCS#7
# signer: Build time autogenerated kernel key
# sig_hashalgo: sha512
OpenSSL 하드웨어 가속
OpenSSL은 CPU 명령어 확장(AES-NI, SHA-NI, AVX-512)과 전용 하드웨어 가속기(QAT, 암호 카드)를 활용하여 암호화 성능을 극대화합니다. OpenSSL 3.x에서는 Default Provider가 CPU 확장을 자동으로 감지하고, 외부 가속기는 Third-Party Provider를 통해 연동합니다.
AES-NI 자동 활용
x86/x86_64 CPU에서 AES-NI(Advanced Encryption Standard New Instructions)가 지원되면
OpenSSL은 빌드 시 자동으로 AESNI 어셈블리 루틴을 포함합니다.
런타임에 CPUID 확인을 통해 AES-NI 가용 여부를 판단하고, 가용하면 하드웨어 경로를 사용합니다.
| 구현 | AES-256-GCM 처리량 | CPU 사이클/바이트 | 비고 |
|---|---|---|---|
| AES-NI + AVX-512 (VAES) | ~40 GB/s | ~0.1 | 최신 Xeon (Ice Lake+) |
| AES-NI + AVX2 | ~10 GB/s | ~0.4 | 대부분의 최신 x86 |
| AES-NI (SSE) | ~5 GB/s | ~0.8 | 기본 AES-NI |
| 소프트웨어 (T-table) | ~0.3 GB/s | ~12 | AES-NI 미지원 |
| ARM CE (Cortex-A76+) | ~4 GB/s | ~1.0 | ARM Crypto Extensions |
# AES-NI 지원 확인
grep -o aes /proc/cpuinfo | head -1
# aes
# OpenSSL이 AES-NI를 사용하는지 확인
openssl speed -evp aes-256-gcm 2>&1 | tail -3
# type 16 bytes 64 bytes 256 bytes 1024 bytes 8192 bytes 16384 bytes
# aes-256-gcm 852.3M 2.8G 5.6G 7.9G 9.2G 9.4G
# AES-NI 비활성화하여 비교 (OPENSSL_ia32cap 환경 변수)
OPENSSL_ia32cap="~0x200000200000000" openssl speed -evp aes-256-gcm 2>&1 | tail -3
# aes-256-gcm 98.7M 172.3M 223.8M 267.4M 289.5M 291.7M
# → AES-NI 없이는 ~30배 느림
Intel QAT Provider
Intel QAT(QuickAssist Technology)는 PCIe 카드 또는 SoC 내장 형태의 전용 암호화/압축 가속기입니다.
OpenSSL과의 연동은 qat_provider(3.x) 또는 qat_engine(1.x)을 통해 이루어집니다.
| 항목 | SW (Default Provider) | QAT Provider |
|---|---|---|
| 대칭 암호 | CPU (AES-NI) | QAT HW 가속 |
| 비대칭 (RSA) | CPU | QAT HW 가속 (RSA 2048: ~30K ops/s) |
| 비동기 모드 | 동기 (블로킹) | 비동기 (이벤트 기반) |
| TLS 핸드셰이크 | CPU 부하 | RSA/ECDH 오프로드 |
| 적합한 상황 | 범용 | 대량 TLS 종단, CDN |
# QAT Provider 설치 확인
openssl list -providers 2>&1 | grep -i qat
# name: QAT Provider
# QAT Provider로 RSA 벤치마크
openssl speed -provider qatprovider -provider default \
-async_jobs 64 rsa2048
# sign/s: ~30000 (QAT) vs ~3000 (SW) → 약 10배 향상
# nginx에서 QAT Provider 사용
# nginx.conf:
# ssl_engine qatengine; # 1.x ENGINE 방식
# ssl_conf_command Providers qatprovider; # 3.x Provider 방식
TLS 1.3 핸드셰이크 상세
TLS 1.3(RFC 8446)은 핸드셰이크를 1-RTT(Round-Trip Time)로 단축하고,
모든 핸드셰이크 메시지를 가능한 빨리 암호화하여 메타데이터 노출을 최소화합니다.
OpenSSL은 ssl/statem/ 디렉터리의 상태 머신으로 이 프로토콜을 구현합니다.
TLS 1.2 vs 1.3 핵심 차이
| 항목 | TLS 1.2 | TLS 1.3 |
|---|---|---|
| 핸드셰이크 | 2-RTT | 1-RTT (0-RTT 재개) |
| 키 교환 | RSA, DHE, ECDHE | ECDHE, DHE만 (RSA 키 교환 제거) |
| 암호 스위트 | 수십 개 조합 가능 | 5개만 (AEAD 필수) |
| 핸드셰이크 암호화 | Finished만 암호화 | ServerHello 이후 전체 암호화 |
| PFS | 선택 (ECDHE 사용 시) | 필수 (항상 임시 키) |
| 압축 | 지원 (CRIME 취약) | 제거 |
| 재협상 | 지원 | 제거 (KeyUpdate로 대체) |
| 세션 재개 | Session ID / Ticket | PSK (NewSessionTicket) |
0-RTT Early Data 구현
/* 서버: 0-RTT Early Data 허용 */
SSL_CTX_set_max_early_data(ctx, 16384);
/* 클라이언트: 세션 재개 시 Early Data 전송 */
SSL *ssl = SSL_new(ctx);
SSL_set_session(ssl, saved_session); /* 이전 세션 PSK 설정 */
/* Early Data 쓰기 (핸드셰이크 완료 전) */
size_t written;
int ret = SSL_write_early_data(ssl, request, req_len, &written);
if (ret == 1) {
/* Early Data 전송 성공 → 핸드셰이크 완료 대기 */
SSL_do_handshake(ssl);
}
/* 서버: Early Data 읽기 */
unsigned char buf[16384];
size_t readbytes;
int status = SSL_read_early_data(ssl, buf, sizeof(buf), &readbytes);
switch (status) {
case SSL_READ_EARLY_DATA_SUCCESS:
/* Early Data 수신 — 재전송 공격 방어 필요 */
break;
case SSL_READ_EARLY_DATA_FINISH:
/* 핸드셰이크 완료 — 일반 SSL_read()로 전환 */
break;
case SSL_READ_EARLY_DATA_ERROR:
/* Early Data 거부 (안티 리플레이) */
break;
}
mTLS (상호 TLS 인증)
mTLS(Mutual TLS)는 서버뿐 아니라 클라이언트도 인증서를 제출하여 양방향으로 신원을 검증하는 TLS 설정입니다. 마이크로서비스 간 통신, API 게이트웨이, 제로 트러스트(Zero Trust) 네트워크에서 핵심적으로 사용됩니다.
/* 서버: mTLS 설정 */
SSL_CTX *ctx = SSL_CTX_new(TLS_server_method());
/* 서버 인증서/키 설정 */
SSL_CTX_use_certificate_chain_file(ctx, "server.crt");
SSL_CTX_use_PrivateKey_file(ctx, "server.key", SSL_FILETYPE_PEM);
/* 클라이언트 인증서 필수 요구 */
SSL_CTX_set_verify(ctx,
SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT,
NULL);
/* 클라이언트 인증서 검증용 CA */
SSL_CTX_load_verify_locations(ctx, "client-ca.crt", NULL);
/* 검증 깊이 */
SSL_CTX_set_verify_depth(ctx, 3);
/* 클라이언트: mTLS 설정 */
SSL_CTX *ctx = SSL_CTX_new(TLS_client_method());
/* 서버 인증서 검증 */
SSL_CTX_set_default_verify_paths(ctx);
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL);
/* 클라이언트 인증서/키 제출 */
SSL_CTX_use_certificate_chain_file(ctx, "client.crt");
SSL_CTX_use_PrivateKey_file(ctx, "client.key", SSL_FILETYPE_PEM);
# mTLS 테스트 (s_client / s_server)
# 서버
openssl s_server -cert server.crt -key server.key \
-CAfile client-ca.crt -Verify 1 -accept 4433
# 클라이언트
openssl s_client -connect localhost:4433 \
-cert client.crt -key client.key \
-CAfile server-ca.crt -verify 4
# nginx mTLS 설정
# ssl_client_certificate /etc/nginx/client-ca.crt;
# ssl_verify_client on;
# ssl_verify_depth 2;
mTLS 활용 시나리오
| 시나리오 | 설명 | 도구/프레임워크 |
|---|---|---|
| 서비스 메시 | 마이크로서비스 간 자동 mTLS | Istio, Linkerd, Envoy |
| 제로 트러스트 | 네트워크 위치 불신, 모든 접속 인증 | BeyondCorp, Tailscale, WireGuard |
| IoT 디바이스 | 디바이스 고유 인증서로 접속 제한 | AWS IoT Core, Azure IoT Hub |
| API 게이트웨이 | 파트너 API 호출 시 클라이언트 인증 | Kong, APISIX, HAProxy |
| 데이터베이스 | DB 접속 시 인증서 기반 인증 | PostgreSQL ssl, MySQL require x509 |
인증서 관리와 커널
OpenSSL은 X.509 인증서의 전체 생명주기(키 생성 → CSR → 서명 → 검증 → 폐기)를 관리하는 핵심 도구입니다. 리눅스 커널은 모듈 서명 검증, IMA(Integrity Measurement Architecture), Secure Boot에서 X.509 인증서를 사용하며, 이 인증서의 생성에 OpenSSL이 활용됩니다.
인증서 체인 검증 흐름
인증서 실무 명령어
# 1. RSA 개인키 생성 (4096비트)
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:4096 \
-out server.key
# 2. EC 개인키 생성 (P-256 — TLS 1.3 권장)
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 \
-out server-ec.key
# 3. Ed25519 개인키 생성 (최신 서명 알고리즘)
openssl genpkey -algorithm Ed25519 -out server-ed.key
# 4. CSR(인증서 서명 요청) 생성
openssl req -new -key server.key -out server.csr \
-subj "/C=KR/ST=Seoul/O=Example/CN=example.com" \
-addext "subjectAltName=DNS:example.com,DNS:www.example.com"
# 5. 자체 서명 인증서 (테스트용, 1년)
openssl req -x509 -new -key server.key -days 365 \
-out server.crt -subj "/CN=example.com" \
-addext "subjectAltName=DNS:example.com"
# 6. 인증서 내용 확인
openssl x509 -in server.crt -text -noout
# 7. 인증서 체인 검증
openssl verify -CAfile ca-bundle.crt -untrusted intermediate.crt server.crt
# server.crt: OK
# 8. PEM → DER 변환 (커널 모듈 서명용)
openssl x509 -in cert.pem -outform DER -out cert.der
openssl rsa -in key.pem -outform DER -out key.der
커널 모듈 서명 키 관리
커널 빌드 시 CONFIG_MODULE_SIG=y이면 certs/ 디렉터리에 서명 키가 자동 생성됩니다.
프로덕션 환경에서는 빌드 서버에서 생성되는 임시 키 대신, 별도로 관리되는 키를 사용해야 합니다.
# certs/x509.genkey — 커널 모듈 서명 키 생성 설정
[ req ]
default_bits = 4096
distinguished_name = req_distinguished_name
prompt = no
string_mask = utf8only
x509_extensions = myexts
[ req_distinguished_name ]
O = My Organization
CN = Module Signing Key
emailAddress = admin@example.com
[ myexts ]
basicConstraints = critical,CA:FALSE
keyUsage = digitalSignature
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid
OpenSSL 보안 설정
OpenSSL의 보안 수준은 openssl.cnf 설정과 애플리케이션 코드의 API 호출에 의해 결정됩니다.
잘못된 설정은 취약한 프로토콜이나 알고리즘을 허용하여 보안 사고로 이어질 수 있습니다.
권장 Cipher Suite
| 프로토콜 | 권장 Cipher Suite | 특징 |
|---|---|---|
| TLS 1.3 | TLS_AES_256_GCM_SHA384TLS_CHACHA20_POLY1305_SHA256TLS_AES_128_GCM_SHA256 | 모두 AEAD, PFS 기본 보장 |
| TLS 1.2 (권장) | ECDHE-ECDSA-AES256-GCM-SHA384ECDHE-RSA-AES256-GCM-SHA384ECDHE-ECDSA-CHACHA20-POLY1305 | ECDHE로 PFS, GCM/Poly1305 AEAD |
| 금지 | RC4, DES, 3DES, MD5, NULL, EXPORT | 알려진 취약점 존재 |
/* TLS 1.3 + TLS 1.2 보안 설정 (C 코드) */
SSL_CTX *ctx = SSL_CTX_new(TLS_server_method());
/* 최소 프로토콜: TLS 1.2 (SSLv3, TLS 1.0, 1.1 비활성화) */
SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION);
/* TLS 1.2 cipher suite 설정 */
SSL_CTX_set_cipher_list(ctx,
"ECDHE+AESGCM:ECDHE+CHACHA20:!aNULL:!MD5:!RC4:!3DES");
/* TLS 1.3 ciphersuites (별도 API) */
SSL_CTX_set_ciphersuites(ctx,
"TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256");
/* ECDH 곡선 설정 */
SSL_CTX_set1_groups_list(ctx, "X25519:P-256:P-384");
/* 서버 측 cipher 우선순위 적용 */
SSL_CTX_set_options(ctx, SSL_OP_CIPHER_SERVER_PREFERENCE);
openssl.cnf 보안 강화
# /etc/ssl/openssl.cnf — 보안 강화 설정
[system_default_sect]
# 최소 TLS 1.2
MinProtocol = TLSv1.2
# 취약한 cipher 금지
CipherString = DEFAULT:!RC4:!3DES:!DES:!MD5:!PSK:!aNULL:!eNULL:!EXPORT
# TLS 1.3 ciphersuites
Ciphersuites = TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256
# ECDH 곡선
Groups = X25519:P-256:P-384
# 서명 알고리즘
SignatureAlgorithms = ECDSA+SHA256:RSA-PSS+SHA256:RSA+SHA256
[openssl_init]
ssl_conf = ssl_sect
[ssl_sect]
system_default = system_default_sect
# 현재 보안 수준(Security Level) 확인
openssl ciphers -v -s -tls1_3
# 특정 서버의 TLS 설정 점검
openssl s_client -connect example.com:443 -tls1_3 \
-brief 2>&1 | head -10
# CONNECTION ESTABLISHED
# Protocol version: TLSv1.3
# Ciphersuite: TLS_AES_256_GCM_SHA384
# Peer certificate: CN = example.com
# 취약한 프로토콜 테스트 (거부되어야 함)
openssl s_client -connect example.com:443 -tls1 2>&1 | grep -i error
# error: ... tlsv1 alert protocol version
QUIC와 HTTP/3 지원
QUIC(RFC 9000)은 UDP 위에 구축된 전송 프로토콜로, TLS 1.3을 프로토콜 내부에 통합하여 연결 수립 지연을 0-RTT까지 줄입니다. OpenSSL 3.2부터 QUIC 프로토콜의 서버/클라이언트 구현을 포함합니다.
QUIC vs TCP+TLS 비교
| 항목 | TCP + TLS 1.3 | QUIC (UDP + 내장 TLS) |
|---|---|---|
| 연결 수립 | TCP 3-way + TLS 1-RTT = 2-RTT | 1-RTT (0-RTT 재개) |
| HOL 블로킹 | TCP 레벨에서 발생 | 스트림별 독립 (HOL 없음) |
| 멀티플렉싱 | HTTP/2 (TCP 위) | 네이티브 스트림 멀티플렉싱 |
| 연결 마이그레이션 | IP 변경 시 재연결 | Connection ID로 유지 |
| 암호화 범위 | 페이로드만 | 헤더 포함 거의 전체 |
| 커널 지원 | kTLS 오프로드 | 현재 사용자 공간만 |
| NAT/방화벽 | 투명 통과 | UDP 차단 환경 주의 |
/* OpenSSL 3.2+ QUIC 클라이언트 예제 */
SSL_CTX *ctx = SSL_CTX_new(OSSL_QUIC_client_method());
SSL_CTX_set_default_verify_paths(ctx);
SSL *ssl = SSL_new(ctx);
SSL_set1_host(ssl, "example.com");
/* UDP 소켓 연결 */
BIO *bio = BIO_new_dgram(udp_fd, BIO_NOCLOSE);
SSL_set0_rbio(ssl, bio);
SSL_set0_wbio(ssl, BIO_up_ref(bio) ? bio : NULL);
/* QUIC 핸드셰이크 */
if (SSL_connect(ssl) <= 0) {
ERR_print_errors_fp(stderr);
return 1;
}
/* QUIC 스트림 생성 (HTTP/3용) */
SSL *stream = SSL_new_stream(ssl, 0); /* 양방향 스트림 */
SSL_write(stream, request, req_len);
char buf[4096];
int n = SSL_read(stream, buf, sizeof(buf));
SSL_free(stream);
SSL_shutdown(ssl);
SSL_free(ssl);
SSL_CTX_free(ctx);
OpenSSL 성능 최적화
openssl speed 벤치마크
openssl speed 명령은 현재 시스템에서 각 알고리즘의 암호화 처리량을 측정합니다.
하드웨어 가속이 올바르게 적용되는지 확인하는 가장 빠른 방법입니다.
# 주요 알고리즘 벤치마크
openssl speed -evp aes-128-gcm aes-256-gcm chacha20-poly1305
# 멀티스레드 벤치마크 (4코어)
openssl speed -evp aes-256-gcm -multi 4
# RSA 서명/검증 벤치마크
openssl speed rsa2048 rsa4096
# ECDSA 벤치마크
openssl speed ecdsap256 ecdsap384
# 비동기 모드 벤치마크 (QAT Provider 사용 시)
openssl speed -evp rsa2048 -async_jobs 32
# 결과 예시 (x86_64, AES-NI 활성)
# type 16 bytes 64 bytes 256 bytes 1024 bytes 8192 bytes 16384 bytes
# aes-256-gcm 852.3M 2.8G 5.6G 7.9G 9.2G 9.4G
# chacha20-poly 654.1M 2.1G 4.2G 5.7G 6.5G 6.6G
비동기 연산 (Async Jobs)
OpenSSL 3.x는 비동기 작업(Async Jobs)을 지원하여, 하드웨어 가속기(QAT 등)에 연산을 제출한 후 완료를 기다리는 동안 다른 작업을 처리할 수 있습니다. 이는 대량의 TLS 핸드셰이크를 처리하는 서버에서 CPU 활용도를 극대화합니다.
/* 비동기 모드 설정 */
SSL_CTX_set_mode(ctx, SSL_MODE_ASYNC);
/* 비동기 작업 대기 fd 사용 (이벤트 루프 통합) */
OSSL_ASYNC_FD *fds;
size_t numfds;
int ret = SSL_do_handshake(ssl);
if (ret == 0 && SSL_get_error(ssl, ret) == SSL_ERROR_WANT_ASYNC) {
/* 비동기 작업 진행 중 — fd를 epoll에 등록 */
SSL_get_all_async_fds(ssl, NULL, &numfds);
fds = OPENSSL_malloc(numfds * sizeof(OSSL_ASYNC_FD));
SSL_get_all_async_fds(ssl, fds, &numfds);
/* epoll_ctl(epfd, EPOLL_CTL_ADD, fds[0], ...) */
}
멀티스레딩 고려사항
OpenSSL 3.x는 스레드 안전(Thread-Safe)하게 설계되어 있으며,
SSL_CTX는 여러 스레드에서 공유할 수 있지만, SSL 객체는 단일 스레드에서만 사용해야 합니다.
| 객체 | 스레드 안전성 | 권장 패턴 |
|---|---|---|
SSL_CTX | 안전 (읽기 전용 후) | 프로세스당 1개, 초기화 후 공유 |
SSL | 안전하지 않음 | 커넥션당 1개, 단일 스레드 |
EVP_MD_CTX | 안전하지 않음 | 스레드 로컬 또는 뮤텍스 보호 |
OSSL_LIB_CTX | 안전 | Provider 격리용 (FIPS 전용 등) |
ERR 상태 | 스레드 로컬 | 스레드별 독립 에러 큐 |
OpenSSL 커맨드라인 도구
키 생성
# RSA 4096비트 키 생성
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:4096 -out rsa.key
# EC P-256 키 생성 (TLS 1.3 권장)
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out ec.key
# Ed25519 키 생성 (최신, 빠른 서명)
openssl genpkey -algorithm Ed25519 -out ed25519.key
# X25519 키 생성 (키 교환 전용)
openssl genpkey -algorithm X25519 -out x25519.key
# 키 정보 확인
openssl pkey -in ec.key -text -noout
# 공개키 추출
openssl pkey -in ec.key -pubout -out ec.pub
인증서 및 CSR
# CSR 생성 (SAN 포함)
openssl req -new -key server.key -out server.csr \
-subj "/C=KR/ST=Seoul/L=Gangnam/O=MyOrg/CN=example.com" \
-addext "subjectAltName=DNS:example.com,DNS:*.example.com,IP:10.0.0.1"
# CSR 내용 확인
openssl req -in server.csr -text -noout -verify
# 자체 서명 Root CA 생성
openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 \
-out ca.crt -subj "/C=KR/O=MyOrg/CN=My Root CA" \
-addext "basicConstraints=critical,CA:TRUE" \
-addext "keyUsage=critical,keyCertSign,cRLSign"
# CA로 서버 인증서 서명
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key \
-CAcreateserial -out server.crt -days 365 -sha256 \
-copy_extensions copy
# 인증서 체인 검증
openssl verify -CAfile ca.crt server.crt
# PKCS#12 번들 생성 (키 + 인증서 + 체인)
openssl pkcs12 -export -out server.p12 \
-inkey server.key -in server.crt -certfile ca.crt
TLS 디버깅
# 서버 TLS 연결 테스트
openssl s_client -connect example.com:443 -servername example.com \
-brief -status
# CONNECTION ESTABLISHED
# Protocol version: TLSv1.3
# Ciphersuite: TLS_AES_256_GCM_SHA384
# Server certificate: CN = example.com
# OCSP response: ... (stapling 정보)
# 인증서 체인 출력
openssl s_client -connect example.com:443 -showcerts 2>/dev/null | \
openssl x509 -text -noout | grep -E "Issuer:|Subject:|Not After"
# 특정 프로토콜/cipher 테스트
openssl s_client -connect example.com:443 -tls1_3 \
-ciphersuites TLS_AES_256_GCM_SHA384
# 간이 TLS 서버 실행 (테스트용)
openssl s_server -cert server.crt -key server.key -accept 4433 \
-tls1_3 -www
# 인증서 만료일 확인
echo | openssl s_client -connect example.com:443 2>/dev/null | \
openssl x509 -noout -enddate
# notAfter=Dec 31 23:59:59 2026 GMT
# 인증서 핑거프린트
openssl x509 -in cert.pem -fingerprint -sha256 -noout
# sha256 Fingerprint=AB:CD:EF:...
OpenSSL vs 커널 Crypto API 비교
OpenSSL(사용자 공간)과 커널 Crypto API(커널 공간) 중 어떤 것을 사용할지는 유스케이스에 따라 달라집니다. 아래 표는 주요 판단 기준을 정리합니다.
| 기준 | OpenSSL (libcrypto) | 커널 Crypto API |
|---|---|---|
| 실행 공간 | 사용자 공간 | 커널 공간 |
| 언어 | C (사용자 프로그램 링크) | C (커널 모듈 전용) |
| TLS 지원 | 완전한 TLS 스택 | kTLS (데이터 경로만) |
| 인증서 관리 | X.509 전체 스택 | 제한적 (모듈 서명 검증) |
| FIPS 인증 | OpenSSL FIPS Provider | 별도 인증 필요 |
| HW 가속 | AES-NI 직접, QAT Provider | 등록된 모든 HW 드라이버 |
| 제로 카피 | 불가 | AF_ALG splice(), kTLS sendfile() |
| 적합한 상황 | 웹 서버, API 서버, CLI 도구 | IPsec, dm-crypt, NFS/SMB, kTLS |
| 오버헤드 | 없음 (직접 링크) | 시스템 호출 (AF_ALG) |
| 디버깅 | gdb, valgrind, strace | ftrace, bpftrace, crash |
구현 시 자주 틀리는 지점
| 실수 | 문제 | 올바른 방법 |
|---|---|---|
SSL_CTX_set_cipher_list("ALL") |
NULL, EXPORT, RC4 등 취약 cipher 허용 | "ECDHE+AESGCM:ECDHE+CHACHA20:!aNULL:!MD5" |
SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL) |
서버 인증서 미검증 — MITM 가능 | SSL_VERIFY_PEER + CA 경로 설정 |
| 호스트명 검증 누락 | 인증서가 유효해도 다른 도메인 인증서 수용 | SSL_set1_host(ssl, "example.com") |
| ERR 큐 미소비 | 이전 에러가 다음 연산의 에러와 혼동 | 에러 처리 후 ERR_clear_error() |
RAND_bytes() 반환값 미확인 |
엔트로피 부족 시 예측 가능한 난수 | 반환값 1 확인, 실패 시 재시도 또는 중단 |
| OpenSSL 1.x ENGINE 코드 그대로 사용 | 3.x에서 deprecated 경고, 향후 제거 | Provider API로 마이그레이션 |
| EVP 컨텍스트 재사용 없이 매번 생성 | 불필요한 메모리 할당/해제 오버헤드 | EVP_MD_CTX_reset()으로 재사용 |
SSL_shutdown() 1회만 호출 |
양방향 종료 미완료 — 세션 재사용 불가 | 반환값 0이면 2차 호출 필요 |
/* 올바른 TLS 클라이언트 검증 설정 */
SSL_CTX *ctx = SSL_CTX_new(TLS_client_method());
/* 1. 시스템 CA 인증서 로드 */
SSL_CTX_set_default_verify_paths(ctx);
/* 2. 서버 인증서 검증 활성화 */
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL);
/* 3. 검증 깊이 제한 */
SSL_CTX_set_verify_depth(ctx, 4);
/* 4. 최소 프로토콜 설정 */
SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION);
/* SSL 객체에서 호스트명 검증 */
SSL *ssl = SSL_new(ctx);
SSL_set1_host(ssl, "example.com");
SSL_set_hostflags(ssl, X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS);
EVP API 심층 분석
EVP(Envelope) API는 OpenSSL의 모든 암호화 연산을 감싸는 고수준 통합 인터페이스입니다. 알고리즘에 독립적인 코드를 작성할 수 있으며, Provider가 런타임에 최적의 구현을 디스패치합니다. OpenSSL 3.x에서는 저수준 API(RSA_*, DH_*, EC_*)가 deprecated되어 모든 암호 연산은 EVP를 통해야 합니다.
Fetch API vs Legacy API
OpenSSL 3.x에서는 Fetch API가 알고리즘을 선택하는 권장 방법입니다.
기존 EVP_sha256() 같은 단축 함수는 내부적으로 기본 컨텍스트(NULL)의 Default Provider에서 fetch하지만,
명시적 fetch를 사용하면 OSSL_LIB_CTX와 property query로 Provider를 제어할 수 있습니다.
| 방식 | 예시 | Provider 제어 | 메모리 관리 | 권장 여부 |
|---|---|---|---|---|
| Fetch API | EVP_MD_fetch(ctx, "SHA2-256", "fips=yes") |
완전한 제어 (컨텍스트 + 프로퍼티) | EVP_MD_free() 필수 |
권장 |
| 단축 함수 | EVP_sha256() |
기본 컨텍스트만 | 정적 객체 (free 불필요) | 간단한 경우 가능 |
| 이름 기반 | EVP_get_digestbyname("SHA256") |
없음 (OBJ 테이블) | 정적 (free 불필요) | deprecated (3.x) |
/* Fetch API — 완전한 Provider 제어 */
OSSL_LIB_CTX *libctx = OSSL_LIB_CTX_new();
OSSL_PROVIDER_load(libctx, "fips");
OSSL_PROVIDER_load(libctx, "base");
/* FIPS Provider의 AES-256-GCM 구현을 명시적으로 선택 */
EVP_CIPHER *cipher = EVP_CIPHER_fetch(
libctx, /* OSSL_LIB_CTX (NULL이면 기본) */
"AES-256-GCM", /* 알고리즘 이름 */
"fips=yes" /* property query */
);
/* 사용 후 반드시 해제 (fetch된 객체는 참조 카운팅) */
EVP_CIPHER_free(cipher);
OSSL_LIB_CTX_free(libctx);
EVP_CIPHER — 대칭 암호화 상세
EVP_CIPHER_CTX는 대칭 암호화의 모든 상태를 보관합니다: 알고리즘, 키, IV, 패딩 모드, 진행 중인 블록 데이터.
한 컨텍스트는 암호화 또는 복호화 중 하나만 담당하며, EVP_CIPHER_CTX_reset()으로 재사용할 수 있습니다.
/* AES-256-GCM 암호화 — 전체 에러 처리 포함 */
#include <openssl/evp.h>
#include <openssl/err.h>
int aes_gcm_encrypt(const unsigned char *pt, int pt_len,
const unsigned char *aad, int aad_len,
const unsigned char *key,
const unsigned char *iv, int iv_len,
unsigned char *ct, int *ct_len,
unsigned char *tag, int tag_len)
{
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
if (!ctx) return 0;
int len, ret = 0;
/* 1단계: 알고리즘만 설정 (키/IV는 아직) */
if (EVP_EncryptInit_ex2(ctx, EVP_aes_256_gcm(),
NULL, NULL, NULL) != 1)
goto err;
/* 2단계: IV 길이 설정 (기본 12바이트, 다른 길이도 가능) */
if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN,
iv_len, NULL) != 1)
goto err;
/* 3단계: 키 + IV 설정 */
if (EVP_EncryptInit_ex2(ctx, NULL, key, iv, NULL) != 1)
goto err;
/* 4단계: AAD 입력 (출력 없음 — ct에 NULL 전달) */
if (aad && aad_len > 0) {
if (EVP_EncryptUpdate(ctx, NULL, &len, aad, aad_len) != 1)
goto err;
}
/* 5단계: 평문 암호화 (스트리밍 가능 — 여러 번 호출) */
if (EVP_EncryptUpdate(ctx, ct, &len, pt, pt_len) != 1)
goto err;
*ct_len = len;
/* 6단계: 최종화 (GCM은 추가 출력 없음) */
if (EVP_EncryptFinal_ex(ctx, ct + *ct_len, &len) != 1)
goto err;
*ct_len += len;
/* 7단계: 인증 태그 추출 */
if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG,
tag_len, tag) != 1)
goto err;
ret = 1;
err:
if (!ret) ERR_print_errors_fp(stderr);
EVP_CIPHER_CTX_free(ctx);
return ret;
}
/* AES-256-GCM 복호화 — 태그 검증 포함 */
int aes_gcm_decrypt(const unsigned char *ct, int ct_len,
const unsigned char *aad, int aad_len,
const unsigned char *tag, int tag_len,
const unsigned char *key,
const unsigned char *iv, int iv_len,
unsigned char *pt, int *pt_len)
{
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
int len, ret = 0;
EVP_DecryptInit_ex2(ctx, EVP_aes_256_gcm(), NULL, NULL, NULL);
EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, iv_len, NULL);
EVP_DecryptInit_ex2(ctx, NULL, key, iv, NULL);
if (aad && aad_len > 0)
EVP_DecryptUpdate(ctx, NULL, &len, aad, aad_len);
EVP_DecryptUpdate(ctx, pt, &len, ct, ct_len);
*pt_len = len;
/* 태그 설정 — Final()에서 검증됨 */
EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG,
tag_len, (void *)tag);
/* Final(): 태그 불일치 시 실패 (ret != 1) */
ret = EVP_DecryptFinal_ex(ctx, pt + *pt_len, &len);
if (ret > 0) {
*pt_len += len;
} else {
/* ⚠ 인증 실패 — 복호화된 데이터를 사용하면 안 됨 */
OPENSSL_cleanse(pt, *pt_len);
*pt_len = 0;
}
EVP_CIPHER_CTX_free(ctx);
return ret > 0;
}
대칭 암호 모드별 특성
| 모드 | API 차이 | 패딩 | 인증 | 병렬화 | 용도 |
|---|---|---|---|---|---|
| GCM | ctrl(SET_IVLEN), ctrl(GET_TAG), AAD 별도 | 불필요 (스트림) | GMAC 태그 | 암호화 가능 | TLS 1.2/1.3, IPsec |
| CCM | ctrl(SET_IVLEN), ctrl(SET_TAG) 필수, 길이 사전 지정 | 불필요 | CBC-MAC 태그 | 불가 | IEEE 802.15.4, BLE |
| CBC | 패딩 주의 (PKCS#7 기본) | 필요 (마지막 블록) | 없음 | 복호화만 | 레거시 호환 |
| CTR | 패딩 불필요, 카운터 관리 | 불필요 (스트림) | 없음 | 암/복호화 | 디스크 암호화 |
| XTS | 키 2배 (256→512비트), tweak value | ciphertext stealing | 없음 | 블록 독립 | 디스크 섹터 암호화 |
| WRAP | AES Key Wrap (RFC 3394) | 내장 | 내장 IV 검증 | 불가 | 키 래핑 |
EVP_MD — 해시/다이제스트 상세
/* 해시 — 기본 패턴과 스트리밍 */
EVP_MD_CTX *mdctx = EVP_MD_CTX_new();
EVP_MD *md = EVP_MD_fetch(NULL, "SHA3-256", NULL);
EVP_DigestInit_ex2(mdctx, md, NULL);
/* 스트리밍: 데이터를 청크 단위로 입력 가능 */
while ((n = read(fd, buf, sizeof(buf))) > 0)
EVP_DigestUpdate(mdctx, buf, n);
unsigned char digest[EVP_MAX_MD_SIZE];
unsigned int digest_len;
EVP_DigestFinal_ex(mdctx, digest, &digest_len);
/* 컨텍스트 재사용 (메모리 재할당 없이) */
EVP_MD_CTX_reset(mdctx);
EVP_DigestInit_ex2(mdctx, md, NULL); /* 새 해시 시작 */
/* 알고리즘 메타데이터 조회 */
int block_size = EVP_MD_get_block_size(md); /* SHA3-256: 136 */
int md_size = EVP_MD_get_size(md); /* SHA3-256: 32 */
const char *name = EVP_MD_get0_name(md); /* "SHA3-256" */
EVP_MD_CTX_free(mdctx);
EVP_MD_free(md);
EVP_MAC — 메시지 인증 코드
/* HMAC-SHA256 계산 */
EVP_MAC *mac = EVP_MAC_fetch(NULL, "HMAC", NULL);
EVP_MAC_CTX *mctx = EVP_MAC_CTX_new(mac);
/* HMAC은 내부 해시 알고리즘 지정이 필요 → OSSL_PARAM 사용 */
OSSL_PARAM params[] = {
OSSL_PARAM_construct_utf8_string(
"digest", "SHA2-256", 0),
OSSL_PARAM_construct_end()
};
EVP_MAC_init(mctx, key, key_len, params);
EVP_MAC_update(mctx, data, data_len);
unsigned char mac_val[32];
size_t mac_len;
EVP_MAC_final(mctx, mac_val, &mac_len, sizeof(mac_val));
EVP_MAC_CTX_free(mctx);
EVP_MAC_free(mac);
/* CMAC-AES 계산 */
EVP_MAC *cmac = EVP_MAC_fetch(NULL, "CMAC", NULL);
EVP_MAC_CTX *cctx = EVP_MAC_CTX_new(cmac);
OSSL_PARAM params[] = {
OSSL_PARAM_construct_utf8_string(
"cipher", "AES-256-CBC", 0),
OSSL_PARAM_construct_end()
};
EVP_MAC_init(cctx, key, 32, params);
EVP_MAC_update(cctx, data, data_len);
EVP_MAC_final(cctx, tag, &tag_len, 16);
EVP_MAC_CTX_free(cctx);
EVP_MAC_free(cmac);
EVP_KDF — 키 파생 함수
/* HKDF (RFC 5869) — TLS 1.3 키 파생에 사용 */
EVP_KDF *kdf = EVP_KDF_fetch(NULL, "HKDF", NULL);
EVP_KDF_CTX *kctx = EVP_KDF_CTX_new(kdf);
OSSL_PARAM params[] = {
OSSL_PARAM_construct_utf8_string(
"digest", "SHA2-256", 0),
OSSL_PARAM_construct_octet_string(
"key", ikm, ikm_len),
OSSL_PARAM_construct_octet_string(
"salt", salt, salt_len),
OSSL_PARAM_construct_octet_string(
"info", info, info_len),
OSSL_PARAM_construct_end()
};
unsigned char okm[32]; /* Output Keying Material */
EVP_KDF_derive(kctx, okm, sizeof(okm), params);
EVP_KDF_CTX_free(kctx);
EVP_KDF_free(kdf);
/* PBKDF2 — 비밀번호 기반 키 파생 */
EVP_KDF *kdf = EVP_KDF_fetch(NULL, "PBKDF2", NULL);
EVP_KDF_CTX *kctx = EVP_KDF_CTX_new(kdf);
unsigned int iterations = 600000; /* OWASP 권장: SHA-256에 600K */
OSSL_PARAM params[] = {
OSSL_PARAM_construct_utf8_string(
"digest", "SHA2-256", 0),
OSSL_PARAM_construct_octet_string(
"pass", password, pass_len),
OSSL_PARAM_construct_octet_string(
"salt", salt, 16),
OSSL_PARAM_construct_uint(
"iter", &iterations),
OSSL_PARAM_construct_end()
};
unsigned char derived_key[32];
EVP_KDF_derive(kctx, derived_key, 32, params);
EVP_KDF_CTX_free(kctx);
EVP_KDF_free(kdf);
EVP_PKEY API — 비대칭 키 관리 심층
EVP_PKEY는 RSA, EC, Ed25519, X25519, DH 등 모든 비대칭 키를 통합으로 래핑하는 컨테이너입니다.
3.x에서는 저수준 API(RSA_*, EC_KEY_*)가 deprecated되어,
키 생성·서명·검증·암호화·키 교환 모두 EVP_PKEY_CTX를 통해 수행합니다.
키 생성 상세
/* EC P-256 키 쌍 생성 (3.x 방식) */
EVP_PKEY_CTX *pctx = EVP_PKEY_CTX_new_from_name(
NULL, "EC", NULL);
EVP_PKEY_keygen_init(pctx);
/* OSSL_PARAM으로 곡선 지정 */
OSSL_PARAM params[] = {
OSSL_PARAM_construct_utf8_string(
"group", "P-256", 0),
OSSL_PARAM_construct_end()
};
EVP_PKEY_CTX_set_params(pctx, params);
EVP_PKEY *pkey = NULL;
EVP_PKEY_keygen(pctx, &pkey);
/* 키 정보 조회 */
int bits = EVP_PKEY_get_bits(pkey); /* 256 */
int security = EVP_PKEY_get_security_bits(pkey); /* 128 */
const char *name = EVP_PKEY_get0_type_name(pkey); /* "EC" */
/* PEM 형식으로 저장 */
BIO *bio = BIO_new_file("ec-key.pem", "w");
PEM_write_bio_PrivateKey(bio, pkey, NULL, NULL, 0, NULL, NULL);
BIO_free(bio);
EVP_PKEY_free(pkey);
EVP_PKEY_CTX_free(pctx);
서명과 검증 (RSA-PSS)
/* RSA-PSS 서명 (DigestSign 통합 API) */
EVP_MD_CTX *mdctx = EVP_MD_CTX_new();
EVP_PKEY_CTX *pctx = NULL;
/* DigestSignInit: 해시 + 서명 알고리즘 + 키 한번에 설정 */
EVP_DigestSignInit_ex(mdctx, &pctx,
"SHA2-256", /* 해시 알고리즘 */
NULL, /* OSSL_LIB_CTX */
NULL, /* property query */
pkey, /* 서명 키 */
NULL);
/* PSS 패딩 설정 (pctx는 mdctx에 종속, 별도 해제 불필요) */
EVP_PKEY_CTX_set_rsa_padding(pctx, RSA_PKCS1_PSS_PADDING);
EVP_PKEY_CTX_set_rsa_pss_saltlen(pctx, RSA_PSS_SALTLEN_DIGEST);
/* 데이터 입력 (스트리밍) */
EVP_DigestSignUpdate(mdctx, data, data_len);
/* 서명 길이 조회 후 서명 생성 */
size_t sig_len;
EVP_DigestSignFinal(mdctx, NULL, &sig_len);
unsigned char *sig = OPENSSL_malloc(sig_len);
EVP_DigestSignFinal(mdctx, sig, &sig_len);
EVP_MD_CTX_free(mdctx);
/* --- 검증 측 --- */
EVP_MD_CTX *vctx = EVP_MD_CTX_new();
EVP_DigestVerifyInit_ex(vctx, &pctx,
"SHA2-256", NULL, NULL, pubkey, NULL);
EVP_PKEY_CTX_set_rsa_padding(pctx, RSA_PKCS1_PSS_PADDING);
EVP_DigestVerifyUpdate(vctx, data, data_len);
int result = EVP_DigestVerifyFinal(vctx, sig, sig_len);
/* result == 1: 검증 성공, 0: 실패, <0: 에러 */
EVP_MD_CTX_free(vctx);
OPENSSL_free(sig);
키 교환 (ECDH / X25519)
/* X25519 ECDH 키 교환 */
/* 양측이 각자 키 쌍 생성 */
EVP_PKEY *my_key = NULL, *peer_key = NULL;
EVP_PKEY_CTX *kctx;
/* 내 키 생성 */
kctx = EVP_PKEY_CTX_new_from_name(NULL, "X25519", NULL);
EVP_PKEY_keygen_init(kctx);
EVP_PKEY_keygen(kctx, &my_key);
EVP_PKEY_CTX_free(kctx);
/* 상대방 공개키 수신 (peer_key는 공개키만 포함) */
/* ... 네트워크에서 수신 후 d2i_PUBKEY() 등으로 로드 ... */
/* 공유 비밀 파생 */
EVP_PKEY_CTX *dctx = EVP_PKEY_CTX_new_from_pkey(
NULL, my_key, NULL);
EVP_PKEY_derive_init(dctx);
EVP_PKEY_derive_set_peer(dctx, peer_key);
/* 공유 비밀 길이 조회 후 파생 */
size_t secret_len;
EVP_PKEY_derive(dctx, NULL, &secret_len);
unsigned char *secret = OPENSSL_malloc(secret_len);
EVP_PKEY_derive(dctx, secret, &secret_len);
/* secret은 32바이트 — HKDF로 실제 키 파생 필요 */
EVP_PKEY_CTX_free(dctx);
OPENSSL_clear_free(secret, secret_len); /* 비밀 데이터 안전 해제 */
SSL/TLS API 심층 분석
SSL_CTX와 SSL은 libssl의 핵심 객체입니다.
SSL_CTX는 TLS 설정의 공장(Factory)으로 프로세스 수명 동안 유지되며,
SSL은 개별 연결의 상태 머신으로 연결당 하나씩 생성됩니다.
비블로킹(Non-blocking) TLS 서버 패턴
/* 비블로킹 TLS 서버 — epoll + OpenSSL */
/* SSL_get_error()로 상태를 확인하고 epoll에 등록 */
int handle_ssl_io(SSL *ssl, int epfd) {
int ret = SSL_do_handshake(ssl);
if (ret == 1) {
/* 핸드셰이크 완료 — 데이터 교환 시작 */
return STATE_DATA;
}
int err = SSL_get_error(ssl, ret);
switch (err) {
case SSL_ERROR_WANT_READ:
/* 소켓에 읽을 데이터 필요 → EPOLLIN 등록 */
epoll_mod(epfd, SSL_get_fd(ssl), EPOLLIN);
return STATE_HANDSHAKE;
case SSL_ERROR_WANT_WRITE:
/* 소켓에 쓸 공간 필요 → EPOLLOUT 등록 */
epoll_mod(epfd, SSL_get_fd(ssl), EPOLLOUT);
return STATE_HANDSHAKE;
case SSL_ERROR_WANT_ASYNC:
/* QAT 등 비동기 Provider 대기 중 */
return STATE_ASYNC_WAIT;
case SSL_ERROR_ZERO_RETURN:
/* 상대방이 shutdown 전송 */
return STATE_SHUTDOWN;
case SSL_ERROR_SSL:
/* 프로토콜 에러 (인증서 실패 등) */
ERR_print_errors_fp(stderr);
return STATE_ERROR;
case SSL_ERROR_SYSCALL:
/* 시스템 호출 에러 — errno 확인 */
if (errno == EAGAIN || errno == EWOULDBLOCK)
return STATE_HANDSHAKE; /* 재시도 */
return STATE_ERROR;
default:
return STATE_ERROR;
}
}
주요 콜백 함수
| 콜백 | 설정 API | 호출 시점 | 용도 |
|---|---|---|---|
| verify_callback | SSL_CTX_set_verify() | 인증서 체인 검증 시 각 인증서마다 | 커스텀 검증 로직 (핀닝 등) |
| info_callback | SSL_CTX_set_info_callback() | 상태 전환마다 | 디버깅, 로깅, 모니터링 |
| msg_callback | SSL_CTX_set_msg_callback() | TLS 메시지 송수신 시 | 프로토콜 분석, 키로깅 |
| alpn_select_cb | SSL_CTX_set_alpn_select_cb() | 핸드셰이크 중 ALPN 협상 | h2, http/1.1 프로토콜 선택 |
| servername_cb | SSL_CTX_set_tlsext_servername_callback() | SNI 확장 수신 시 | 가상 호스팅, SSL_CTX 전환 |
| keylog_callback | SSL_CTX_set_keylog_callback() | 키 교환 완료 시 | Wireshark 디코딩용 키 로깅 |
| ticket_key_cb | SSL_CTX_set_tlsext_ticket_key_evp_cb() | 세션 티켓 생성/검증 시 | 분산 서버 세션 공유 |
/* 키 로깅 콜백 — Wireshark 디코딩용 (SSLKEYLOGFILE) */
static void keylog_cb(const SSL *ssl, const char *line)
{
FILE *f = fopen(getenv("SSLKEYLOGFILE"), "a");
if (f) {
fprintf(f, "%s\n", line);
fclose(f);
}
}
SSL_CTX_set_keylog_callback(ctx, keylog_cb);
/* Wireshark: Edit → Preferences → TLS → (Pre)-Master-Secret log */
/* SNI 콜백 — 가상 호스팅 (도메인별 인증서 전환) */
static int sni_cb(SSL *ssl, int *al, void *arg)
{
const char *hostname = SSL_get_servername(
ssl, TLSEXT_NAMETYPE_host_name);
if (!hostname) return SSL_TLSEXT_ERR_NOACK;
/* 도메인에 맞는 SSL_CTX 선택 */
SSL_CTX *new_ctx = lookup_ctx_by_hostname(hostname);
if (new_ctx)
SSL_set_SSL_CTX(ssl, new_ctx);
return SSL_TLSEXT_ERR_OK;
}
SSL_CTX_set_tlsext_servername_callback(ctx, sni_cb);
X509 API — 인증서 파싱과 검증
X509 구조체는 X.509 인증서의 모든 필드를 담고 있으며,
X509_STORE는 신뢰 체인 검증을 수행하는 인증서 저장소입니다.
인증서 파싱 API
/* PEM 파일에서 인증서 로드 및 필드 추출 */
BIO *bio = BIO_new_file("server.crt", "r");
X509 *cert = PEM_read_bio_X509(bio, NULL, NULL, NULL);
BIO_free(bio);
/* Subject (주체) */
X509_NAME *subj = X509_get_subject_name(cert);
char subj_str[256];
X509_NAME_oneline(subj, subj_str, sizeof(subj_str));
/* "/C=KR/O=MyOrg/CN=example.com" */
/* Issuer (발급자) */
X509_NAME *issuer = X509_get_issuer_name(cert);
/* 유효 기간 */
const ASN1_TIME *not_before = X509_get0_notBefore(cert);
const ASN1_TIME *not_after = X509_get0_notAfter(cert);
/* 공개키 */
EVP_PKEY *pubkey = X509_get0_pubkey(cert);
int key_bits = EVP_PKEY_get_bits(pubkey);
/* 시리얼 넘버 */
const ASN1_INTEGER *serial = X509_get0_serialNumber(cert);
/* SAN (Subject Alternative Names) */
GENERAL_NAMES *sans = X509_get_ext_d2i(
cert, NID_subject_alt_name, NULL, NULL);
if (sans) {
for (int i = 0; i < sk_GENERAL_NAME_num(sans); i++) {
GENERAL_NAME *gen = sk_GENERAL_NAME_value(sans, i);
if (gen->type == GEN_DNS) {
const char *dns = (const char *)
ASN1_STRING_get0_data(gen->d.dNSName);
printf("SAN DNS: %s\n", dns);
}
}
GENERAL_NAMES_free(sans);
}
X509_free(cert);
프로그래밍 방식 인증서 검증
/* X509_STORE를 사용한 인증서 체인 검증 */
X509_STORE *store = X509_STORE_new();
/* 시스템 CA 인증서 로드 */
X509_STORE_set_default_paths(store);
/* 또는 특정 CA 파일 로드 */
X509_STORE_load_file(store, "/etc/ssl/certs/ca-certificates.crt");
/* CRL 검증 활성화 (선택) */
X509_STORE_set_flags(store, X509_V_FLAG_CRL_CHECK);
/* 검증 컨텍스트 */
X509_STORE_CTX *vctx = X509_STORE_CTX_new();
X509_STORE_CTX_init(vctx, store, target_cert, untrusted_chain);
/* 검증 실행 */
int result = X509_verify_cert(vctx);
if (result != 1) {
int err = X509_STORE_CTX_get_error(vctx);
int depth = X509_STORE_CTX_get_error_depth(vctx);
fprintf(stderr, "검증 실패 depth=%d: %s\n",
depth, X509_verify_cert_error_string(err));
/* 예: "unable to get local issuer certificate" */
}
/* 검증된 체인 조회 */
STACK_OF(X509) *chain = X509_STORE_CTX_get0_chain(vctx);
printf("체인 깊이: %d\n", sk_X509_num(chain));
X509_STORE_CTX_free(vctx);
X509_STORE_free(store);
OSSL_PARAM — 범용 파라미터 전달
OpenSSL 3.x에서 OSSL_PARAM은 Provider와 CORE 사이에 데이터를 전달하는 범용 구조체입니다.
기존의 ctrl() 호출이나 개별 setter 함수를 대체하여,
모든 설정과 조회를 키-값 배열로 통일합니다.
/* OSSL_PARAM 구성 예시 */
/* 배열의 마지막은 반드시 OSSL_PARAM_END로 종료 */
/* 문자열 파라미터 */
OSSL_PARAM params[4];
params[0] = OSSL_PARAM_construct_utf8_string(
"digest", /* 키 */
"SHA2-256", /* 값 */
0); /* 0 = strlen 자동 계산 */
/* 정수 파라미터 */
unsigned int iter = 100000;
params[1] = OSSL_PARAM_construct_uint(
"iter", &iter);
/* 바이너리 데이터 (키, salt 등) */
params[2] = OSSL_PARAM_construct_octet_string(
"salt", salt_buf, 16);
/* 종료 마커 — 반드시 필요 */
params[3] = OSSL_PARAM_construct_end();
/* 사용: EVP_MAC_init(mctx, key, key_len, params); */
| 생성자 함수 | C 타입 | 용도 예시 |
|---|---|---|
OSSL_PARAM_construct_utf8_string() | char * | 알고리즘 이름, 곡선 이름 |
OSSL_PARAM_construct_octet_string() | void *, size_t | 키, salt, IV, 바이너리 데이터 |
OSSL_PARAM_construct_uint() | unsigned int * | 반복 횟수, 비트 수 |
OSSL_PARAM_construct_int() | int * | 플래그, 옵션 |
OSSL_PARAM_construct_size_t() | size_t * | 출력 길이 |
OSSL_PARAM_construct_BN() | BIGNUM * | RSA 소수, DH 파라미터 |
OSSL_PARAM_construct_end() | — | 배열 종료 (필수) |
/* OSSL_PARAM으로 키 파라미터 조회 (gettable) */
OSSL_PARAM *gettable = EVP_PKEY_gettable_params(pkey);
/* gettable 배열에 조회 가능한 파라미터 목록이 들어 있음 */
/* 예: RSA 키의 공개 지수(e)와 모듈러스(n) 추출 */
BIGNUM *n = NULL, *e = NULL;
EVP_PKEY_get_bn_param(pkey, "n", &n);
EVP_PKEY_get_bn_param(pkey, "e", &e);
printf("RSA modulus bits: %d\n", BN_num_bits(n));
BN_free(n);
BN_free(e);
ERR API — 에러 처리 내부 구조
OpenSSL의 에러 처리는 스레드 로컬 에러 큐(Error Queue) 기반입니다. 각 스레드는 독립된 FIFO 에러 스택을 가지며, 에러는 발생 순서대로 쌓입니다. 에러를 소비(pop)하지 않으면 다음 연산의 에러와 혼동될 수 있습니다.
/* 에러 큐 올바른 사용 패턴 */
/* 1. 연산 전 에러 큐 초기화 (이전 에러 오염 방지) */
ERR_clear_error();
/* 2. 연산 수행 */
int ret = EVP_EncryptFinal_ex(ctx, out, &outl);
/* 3. 실패 시 에러 큐에서 모든 에러 추출 */
if (ret != 1) {
unsigned long err;
while ((err = ERR_get_error()) != 0) {
char buf[256];
ERR_error_string_n(err, buf, sizeof(buf));
fprintf(stderr, "Error: %s\n", buf);
/* "error:0A000126:SSL routines::unexpected eof while reading" */
/* 구조화된 에러 정보 */
int lib = ERR_GET_LIB(err); /* 라이브러리 (SSL, EVP...) */
int reason = ERR_GET_REASON(err); /* 에러 코드 */
const char *file;
int line;
ERR_peek_error_line(&file, &line); /* 소스 위치 */
}
}
/* 4. 에러 큐를 stderr로 한번에 출력 (간편) */
ERR_print_errors_fp(stderr);
/* 5. BIO로 출력 (로깅 시스템 연동) */
BIO *bio_err = BIO_new_fp(stderr, BIO_NOCLOSE);
ERR_print_errors(bio_err);
BIO_free(bio_err);
메모리 관리 API
OpenSSL은 자체 메모리 할당 함수를 제공하여 디버깅, 보안 소거(secure erase), 커스텀 할당기 연동을 지원합니다. 민감한 데이터(키, 비밀번호)를 다룰 때는 반드시 안전한 해제 함수를 사용해야 합니다.
| 함수 | 표준 C 대응 | 추가 기능 |
|---|---|---|
OPENSSL_malloc(size) | malloc(size) | 디버그 추적, 커스텀 할당기 |
OPENSSL_zalloc(size) | calloc(1, size) | 0 초기화 + 디버그 추적 |
OPENSSL_realloc(p, size) | realloc(p, size) | 디버그 추적 |
OPENSSL_free(p) | free(p) | 디버그 추적 |
OPENSSL_clear_free(p, len) | — | 메모리 0으로 소거 후 해제 (키 데이터용) |
OPENSSL_cleanse(p, len) | — | 컴파일러 최적화 방지 메모리 소거 |
OPENSSL_secure_malloc(size) | — | mlock()으로 스왑 방지 + 소거 보장 |
OPENSSL_secure_free(p) | — | secure 영역 해제 |
CRYPTO_set_mem_functions() | — | 커스텀 할당기 등록 (jemalloc 등) |
/* 민감 데이터의 안전한 메모리 관리 */
/* ❌ 위험: free()는 메모리를 소거하지 않음 */
char *password = malloc(64);
strcpy(password, "secret");
free(password); /* 메모리에 "secret" 잔존 → 코어 덤프 시 노출 */
/* ✅ 안전: OPENSSL_clear_free()는 0으로 소거 후 해제 */
char *password = OPENSSL_zalloc(64);
strcpy(password, "secret");
OPENSSL_clear_free(password, 64); /* memset(0) + free */
/* ✅ 최상: secure_malloc은 mlock()으로 스왑 방지 */
unsigned char *key = OPENSSL_secure_malloc(32);
/* ... 키 사용 ... */
OPENSSL_secure_free(key);
/* → 메모리가 디스크 스왑에 쓰여지지 않으며, 해제 시 소거됨 */
커스텀 Provider 구현 심층 가이드
OpenSSL 3.x에서는 자체 암호화 알고리즘이나 하드웨어 가속기를 Provider로 패키징하여
OpenSSL 생태계에 플러그인할 수 있습니다.
Provider는 OSSL_provider_init() 진입점을 구현하는 공유 라이브러리(.so)이며,
OpenSSL CORE가 dlopen()으로 로딩합니다.
Provider 연산 카테고리 (operation_id)
query_operation()에서 CORE가 요청하는 연산 ID별로 해당 알고리즘 목록을 반환합니다.
| operation_id | 카테고리 | 구현해야 할 함수들 | 예시 알고리즘 |
|---|---|---|---|
OSSL_OP_DIGEST | 해시/다이제스트 | newctx, init, update, final, freectx, get_params | SHA-256, SHA3-256 |
OSSL_OP_CIPHER | 대칭 암호 | newctx, encrypt_init, decrypt_init, update, final, freectx, get_params, set_ctx_params | AES-256-GCM |
OSSL_OP_MAC | 메시지 인증 코드 | newctx, init, update, final, freectx | HMAC, CMAC |
OSSL_OP_KDF | 키 파생 함수 | newctx, derive, freectx, set_ctx_params | HKDF, PBKDF2 |
OSSL_OP_KEYMGMT | 키 관리 | new, gen_init, gen, free, has, import, export, get_params | RSA, EC, Ed25519 |
OSSL_OP_KEYEXCH | 키 교환 | newctx, init, set_peer, derive, freectx | ECDH, X25519, DH |
OSSL_OP_SIGNATURE | 디지털 서명 | newctx, sign_init, sign, verify_init, verify, freectx | RSA-PSS, ECDSA |
OSSL_OP_ASYM_CIPHER | 비대칭 암호화 | newctx, encrypt_init, encrypt, decrypt_init, decrypt, freectx | RSA-OAEP |
OSSL_OP_KEM | 키 캡슐화 | newctx, encapsulate, decapsulate, freectx | RSA-KEM, PQC |
OSSL_OP_RAND | 난수 생성 | newctx, instantiate, generate, freectx | CTR-DRBG |
OSSL_OP_STORE | 키/인증서 저장소 | open, load, eof, close | file:, pkcs11: |
OSSL_OP_ENCODER | 직렬화 (출력) | newctx, encode, freectx | PEM, DER |
OSSL_OP_DECODER | 역직렬화 (입력) | newctx, decode, freectx | PEM, DER |
완전한 Digest Provider 구현 예제
/* simple_provider.c — 커스텀 해시 Provider 전체 구현 */
/* 예시: 간단한 XOR 기반 체크섬 (교육용, 암호학적 안전하지 않음) */
#include <string.h>
#include <openssl/core.h>
#include <openssl/core_dispatch.h>
#include <openssl/core_names.h>
#include <openssl/params.h>
/* ============ Provider 컨텍스트 ============ */
typedef struct {
const OSSL_CORE_HANDLE *handle;
OSSL_FUNC_core_get_params_fn *core_get_params;
} MYPROV_CTX;
/* ============ Digest 알고리즘 컨텍스트 ============ */
#define XORSUM_BLOCK_SIZE 64
#define XORSUM_DIGEST_SIZE 4
typedef struct {
MYPROV_CTX *provctx;
unsigned char state[XORSUM_DIGEST_SIZE];
} XORSUM_CTX;
/* ============ Digest 연산 함수들 ============ */
static void *xorsum_newctx(void *provctx)
{
XORSUM_CTX *ctx = OPENSSL_zalloc(sizeof(*ctx));
if (ctx)
ctx->provctx = provctx;
return ctx;
}
static int xorsum_init(void *vctx, const OSSL_PARAM params[])
{
XORSUM_CTX *ctx = vctx;
memset(ctx->state, 0, XORSUM_DIGEST_SIZE);
return 1;
}
static int xorsum_update(void *vctx,
const unsigned char *data, size_t len)
{
XORSUM_CTX *ctx = vctx;
for (size_t i = 0; i < len; i++)
ctx->state[i % XORSUM_DIGEST_SIZE] ^= data[i];
return 1;
}
static int xorsum_final(void *vctx,
unsigned char *out, size_t *outl, size_t outsz)
{
XORSUM_CTX *ctx = vctx;
if (outsz < XORSUM_DIGEST_SIZE)
return 0;
memcpy(out, ctx->state, XORSUM_DIGEST_SIZE);
*outl = XORSUM_DIGEST_SIZE;
return 1;
}
static void xorsum_freectx(void *vctx)
{
XORSUM_CTX *ctx = vctx;
OPENSSL_clear_free(ctx, sizeof(*ctx));
}
static void *xorsum_dupctx(void *vctx)
{
XORSUM_CTX *src = vctx;
XORSUM_CTX *dst = OPENSSL_malloc(sizeof(*dst));
if (dst)
*dst = *src;
return dst;
}
/* ============ Digest 파라미터 조회 ============ */
static const OSSL_PARAM *xorsum_gettable_params(void *provctx)
{
static const OSSL_PARAM table[] = {
OSSL_PARAM_size_t(OSSL_DIGEST_PARAM_BLOCK_SIZE, NULL),
OSSL_PARAM_size_t(OSSL_DIGEST_PARAM_SIZE, NULL),
OSSL_PARAM_int(OSSL_DIGEST_PARAM_XOF, NULL),
OSSL_PARAM_int(OSSL_DIGEST_PARAM_ALGID_ABSENT, NULL),
OSSL_PARAM_END
};
return table;
}
static int xorsum_get_params(OSSL_PARAM params[])
{
OSSL_PARAM *p;
if ((p = OSSL_PARAM_locate(params, OSSL_DIGEST_PARAM_BLOCK_SIZE)))
if (!OSSL_PARAM_set_size_t(p, XORSUM_BLOCK_SIZE))
return 0;
if ((p = OSSL_PARAM_locate(params, OSSL_DIGEST_PARAM_SIZE)))
if (!OSSL_PARAM_set_size_t(p, XORSUM_DIGEST_SIZE))
return 0;
if ((p = OSSL_PARAM_locate(params, OSSL_DIGEST_PARAM_XOF)))
if (!OSSL_PARAM_set_int(p, 0))
return 0;
return 1;
}
/* ============ Digest OSSL_DISPATCH 테이블 ============ */
static const OSSL_DISPATCH xorsum_functions[] = {
{ OSSL_FUNC_DIGEST_NEWCTX, (void (*)())xorsum_newctx },
{ OSSL_FUNC_DIGEST_INIT, (void (*)())xorsum_init },
{ OSSL_FUNC_DIGEST_UPDATE, (void (*)())xorsum_update },
{ OSSL_FUNC_DIGEST_FINAL, (void (*)())xorsum_final },
{ OSSL_FUNC_DIGEST_FREECTX, (void (*)())xorsum_freectx },
{ OSSL_FUNC_DIGEST_DUPCTX, (void (*)())xorsum_dupctx },
{ OSSL_FUNC_DIGEST_GET_PARAMS, (void (*)())xorsum_get_params },
{ OSSL_FUNC_DIGEST_GETTABLE_PARAMS,(void (*)())xorsum_gettable_params },
{ 0, NULL }
};
/* ============ Provider 레벨 ============ */
static const OSSL_ALGORITHM my_digests[] = {
{ "XORSUM", /* 알고리즘 이름 */
"provider=simpleprov", /* property 정의 */
xorsum_functions, /* 구현 함수 테이블 */
"XOR checksum (non-crypto)" /* 설명 */
},
{ NULL, NULL, NULL, NULL } /* 종료 */
};
static const OSSL_ALGORITHM *my_query(
void *provctx, int operation_id, int *no_cache)
{
*no_cache = 0;
switch (operation_id) {
case OSSL_OP_DIGEST: return my_digests;
default: return NULL;
}
}
static void my_teardown(void *provctx)
{
OPENSSL_free(provctx);
}
static const OSSL_PARAM *my_gettable_params(void *provctx)
{
static const OSSL_PARAM table[] = {
OSSL_PARAM_DEFN(OSSL_PROV_PARAM_NAME, OSSL_PARAM_UTF8_PTR, NULL, 0),
OSSL_PARAM_DEFN(OSSL_PROV_PARAM_VERSION, OSSL_PARAM_UTF8_PTR, NULL, 0),
OSSL_PARAM_END
};
return table;
}
static int my_get_params(void *provctx, OSSL_PARAM params[])
{
OSSL_PARAM *p;
if ((p = OSSL_PARAM_locate(params, OSSL_PROV_PARAM_NAME)))
if (!OSSL_PARAM_set_utf8_ptr(p, "Simple Example Provider"))
return 0;
if ((p = OSSL_PARAM_locate(params, OSSL_PROV_PARAM_VERSION)))
if (!OSSL_PARAM_set_utf8_ptr(p, "1.0.0"))
return 0;
return 1;
}
static const OSSL_DISPATCH my_dispatch[] = {
{ OSSL_FUNC_PROVIDER_QUERY_OPERATION,
(void (*)())my_query },
{ OSSL_FUNC_PROVIDER_TEARDOWN,
(void (*)())my_teardown },
{ OSSL_FUNC_PROVIDER_GETTABLE_PARAMS,
(void (*)())my_gettable_params },
{ OSSL_FUNC_PROVIDER_GET_PARAMS,
(void (*)())my_get_params },
{ 0, NULL }
};
/* ============ 진입점 ============ */
int OSSL_provider_init(
const OSSL_CORE_HANDLE *handle,
const OSSL_DISPATCH *in,
const OSSL_DISPATCH **out,
void **provctx)
{
/* Provider 컨텍스트 생성 */
MYPROV_CTX *ctx = OPENSSL_zalloc(sizeof(*ctx));
if (!ctx) return 0;
ctx->handle = handle;
/* CORE가 제공하는 함수 추출 (필요한 것만) */
for (; in->function_id != 0; in++) {
switch (in->function_id) {
case OSSL_FUNC_CORE_GET_PARAMS:
ctx->core_get_params =
OSSL_FUNC_core_get_params(in);
break;
}
}
*out = my_dispatch;
*provctx = ctx;
return 1;
}
빌드, 테스트, openssl.cnf 설정
# 빌드
gcc -shared -fPIC -o simpleprov.so simple_provider.c \
$(pkg-config --cflags openssl)
# Provider 정보 확인
openssl list -providers \
-provider-path . -provider simpleprov
# Providers:
# simpleprov
# name: Simple Example Provider
# version: 1.0.0
# status: active
# 알고리즘 목록 확인
openssl list -digest-algorithms \
-provider-path . -provider simpleprov 2>&1 | grep XORSUM
# XORSUM @ simpleprov
# 사용 테스트
echo "Hello" | openssl dgst -provider-path . \
-provider simpleprov -provider default -XORSUM
# XORSUM(stdin)= 2c690a00
# openssl.cnf — 커스텀 Provider 영구 설정
openssl_conf = openssl_init
[openssl_init]
providers = provider_sect
[provider_sect]
# 기본 Provider (항상 필요)
default = default_sect
# 커스텀 Provider
simpleprov = simpleprov_sect
[default_sect]
activate = 1
[simpleprov_sect]
module = /opt/providers/simpleprov.so
activate = 1
Cipher Provider 구현 핵심
Cipher(대칭 암호)를 Provider로 구현하려면 Digest보다 많은 함수가 필요합니다. 암호화/복호화 방향, 키/IV 설정, 패딩, AEAD 태그 등을 처리해야 합니다.
/* Cipher Provider — 필수 OSSL_DISPATCH 함수 목록 */
static const OSSL_DISPATCH my_cipher_functions[] = {
/* 컨텍스트 관리 */
{ OSSL_FUNC_CIPHER_NEWCTX, (void (*)())my_cipher_newctx },
{ OSSL_FUNC_CIPHER_FREECTX, (void (*)())my_cipher_freectx },
{ OSSL_FUNC_CIPHER_DUPCTX, (void (*)())my_cipher_dupctx },
/* 초기화 (방향 지정) */
{ OSSL_FUNC_CIPHER_ENCRYPT_INIT, (void (*)())my_cipher_einit },
{ OSSL_FUNC_CIPHER_DECRYPT_INIT, (void (*)())my_cipher_dinit },
/* 데이터 처리 */
{ OSSL_FUNC_CIPHER_UPDATE, (void (*)())my_cipher_update },
{ OSSL_FUNC_CIPHER_FINAL, (void (*)())my_cipher_final },
/* 파라미터 (키 길이, 블록 크기, IV 길이, 패딩 등) */
{ OSSL_FUNC_CIPHER_GET_PARAMS, (void (*)())my_cipher_get_params },
{ OSSL_FUNC_CIPHER_GETTABLE_PARAMS, (void (*)())my_cipher_gettable_params },
{ OSSL_FUNC_CIPHER_SET_CTX_PARAMS, (void (*)())my_cipher_set_ctx_params },
{ OSSL_FUNC_CIPHER_GET_CTX_PARAMS, (void (*)())my_cipher_get_ctx_params },
{ OSSL_FUNC_CIPHER_SETTABLE_CTX_PARAMS,
(void (*)())my_cipher_settable_ctx_params },
{ OSSL_FUNC_CIPHER_GETTABLE_CTX_PARAMS,
(void (*)())my_cipher_gettable_ctx_params },
{ 0, NULL }
};
/* get_params에서 반환해야 할 필수 파라미터들 */
static int my_cipher_get_params(OSSL_PARAM params[])
{
OSSL_PARAM *p;
/* CORE가 알고리즘 특성을 알기 위해 조회하는 파라미터 */
if ((p = OSSL_PARAM_locate(params, OSSL_CIPHER_PARAM_BLOCK_SIZE)))
OSSL_PARAM_set_size_t(p, 16); /* 128비트 블록 */
if ((p = OSSL_PARAM_locate(params, OSSL_CIPHER_PARAM_KEYLEN)))
OSSL_PARAM_set_size_t(p, 32); /* 256비트 키 */
if ((p = OSSL_PARAM_locate(params, OSSL_CIPHER_PARAM_IVLEN)))
OSSL_PARAM_set_size_t(p, 12); /* GCM 표준 IV */
if ((p = OSSL_PARAM_locate(params, OSSL_CIPHER_PARAM_MODE)))
OSSL_PARAM_set_uint(p, EVP_CIPH_GCM_MODE);
if ((p = OSSL_PARAM_locate(params, OSSL_CIPHER_PARAM_AEAD)))
OSSL_PARAM_set_int(p, 1); /* AEAD 모드 */
return 1;
}
하드웨어 가속기용 Provider 패턴
실제 프로덕션 Provider는 하드웨어 가속기(QAT, CAAM, CCP 등)를 래핑합니다. 이 경우 Provider 컨텍스트에 디바이스 핸들을 보관하고, 각 알고리즘 컨텍스트에서 하드웨어 명령을 제출합니다.
/* HW 가속기 Provider 패턴 (개념 코드) */
typedef struct {
const OSSL_CORE_HANDLE *handle;
int hw_fd; /* /dev/crypto 또는 벤더 디바이스 */
void *hw_ctx; /* 하드웨어 세션 컨텍스트 */
int async_capable; /* 비동기 지원 여부 */
} HW_PROV_CTX;
static int hw_cipher_update(void *vctx,
unsigned char *out, size_t *outl, size_t outsz,
const unsigned char *in, size_t inl)
{
HW_CIPHER_CTX *ctx = vctx;
/* 입력 데이터를 HW DMA 버퍼에 복사 */
memcpy(ctx->hw_inbuf, in, inl);
/* 하드웨어에 암호화 명령 제출 */
struct hw_request req = {
.opcode = HW_OP_AES_GCM_ENCRYPT,
.src = ctx->hw_inbuf_dma,
.dst = ctx->hw_outbuf_dma,
.len = inl,
.key_handle = ctx->hw_key_id,
};
ioctl(ctx->provctx->hw_fd, HW_SUBMIT_REQUEST, &req);
/* 완료 대기 (동기) 또는 폴링 (비동기) */
ioctl(ctx->provctx->hw_fd, HW_WAIT_COMPLETION, &req);
memcpy(out, ctx->hw_outbuf, inl);
*outl = inl;
return 1;
}
/* OSSL_provider_init에서 디바이스 초기화 */
int OSSL_provider_init(const OSSL_CORE_HANDLE *handle,
const OSSL_DISPATCH *in, const OSSL_DISPATCH **out,
void **provctx)
{
HW_PROV_CTX *ctx = OPENSSL_zalloc(sizeof(*ctx));
ctx->handle = handle;
/* 하드웨어 디바이스 열기 */
ctx->hw_fd = open("/dev/crypto_accel", O_RDWR);
if (ctx->hw_fd < 0) {
/* HW 미감지 — 로드 실패 (Default Provider가 대체) */
OPENSSL_free(ctx);
return 0;
}
*out = hw_dispatch;
*provctx = ctx;
return 1;
}
providers/implementations/ 디렉터리에 있는 Default Provider 구현을 참조하는 것이 가장 좋습니다.
특히 providers/implementations/digests/sha2_prov.c와
providers/implementations/ciphers/cipher_aes_gcm.c가 실전 수준의 구현 패턴을 보여줍니다.
OpenSSL vs 다른 TLS 라이브러리
OpenSSL 외에도 여러 TLS/암호화 라이브러리가 존재합니다. 각 라이브러리는 설계 철학, 지원 범위, 라이선스가 다르며, 유스케이스에 따라 적합한 선택이 달라집니다.
| 라이브러리 | 유지 주체 | 주요 특징 | 대표 사용처 | 라이선스 |
|---|---|---|---|---|
| OpenSSL | OpenSSL Software Foundation | 가장 광범위한 알고리즘, Provider 시스템, FIPS 인증 | nginx, Apache, HAProxy, Linux 커널(sign-file) | Apache 2.0 |
| BoringSSL | OpenSSL 포크, API 안정성 비보장, QUIC 선도 지원 | Chrome, Android, gRPC, Envoy | ISC-style | |
| LibreSSL | OpenBSD | OpenSSL 포크, 보안 강화(레거시 제거), libtls API | OpenBSD, macOS (일부), Alpine Linux | ISC + BSD |
| wolfSSL | wolfSSL Inc. | 경량 임베디드, FIPS 인증, TLS 1.3, DTLS 1.3 | IoT, 자동차, 의료기기, RTOS | GPLv2 / 상용 |
| GnuTLS | GNU 프로젝트 | LGPL, PKCS#11 네이티브, SRP/PSK | GNOME, GLib 에코시스템, CUPS | LGPLv2.1+ |
| mbedTLS | ARM (Mbed) | 경량 임베디드, PSA Crypto API, TF-M 통합 | ARM Cortex-M, Zephyr, TF-A | Apache 2.0 / GPLv2 |
| rustls | ISRG (Let's Encrypt) | Rust 구현, 메모리 안전, ring 암호 백엔드 | curl (실험), hyper, Cloudflare | Apache 2.0 / MIT |
| s2n-tls | AWS | TLS만(암호 라이브러리 아님), 최소 공격면, 정형 검증 | AWS SDK, Elastic Load Balancer | Apache 2.0 |
sign-file(모듈 서명)은 libcrypto(OpenSSL)를 직접 링크합니다.
커널 빌드 시스템은 OpenSSL만 지원하므로, 다른 라이브러리로 대체할 수 없습니다.
반면 사용자 공간 애플리케이션(nginx, curl 등)은 빌드 시 TLS 백엔드를 선택할 수 있습니다.
BoringSSL과 OpenSSL의 API 차이
BoringSSL은 OpenSSL에서 포크되었지만, API 호환성을 보장하지 않습니다. 주요 차이점을 이해하면 라이브러리 마이그레이션 시 발생하는 문제를 예방할 수 있습니다.
| 항목 | OpenSSL 3.x | BoringSSL |
|---|---|---|
| 버전 관리 | 시맨틱 버전 (3.x.y) | Git 커밋 해시만 |
| Provider/ENGINE | Provider 시스템 | 없음 (단일 구현) |
| FIPS | FIPS Provider | BoringCrypto 모듈 |
| QUIC | 3.2+ 내장 | 자체 QUIC API (선도) |
| EVP API | EVP_MD_fetch() 등 3.x API | EVP_sha256() 등 1.1 스타일 |
| 에러 처리 | ERR 큐 (멀티스레드) | 유사하나 일부 코드 다름 |
| ABI 안정성 | 마이너 버전 간 보장 | 보장 없음 |
주요 보안 취약점 히스토리
OpenSSL의 역사적인 취약점들은 TLS 구현 전반의 보안 교훈을 남겼습니다. 각 취약점의 원인과 커널/시스템 수준 영향을 이해하면 방어적 설정에 도움이 됩니다.
| 취약점 | CVE | 연도 | 원인 | 영향 | 교훈 |
|---|---|---|---|---|---|
| Heartbleed | CVE-2014-0160 | 2014 | TLS Heartbeat 확장의 경계 검사 누락 | 서버 메모리 64KB 유출 (개인키, 세션 토큰) | 경계 검사 필수, 메모리 안전 언어 논의 촉발 |
| POODLE | CVE-2014-3566 | 2014 | SSLv3 CBC 패딩 오라클 | 암호문에서 평문 복원 가능 | SSLv3 완전 비활성화, TLS_FALLBACK_SCSV |
| DROWN | CVE-2016-0800 | 2016 | SSLv2 지원이 TLS 세션 해독에 활용 | SSLv2 지원 서버의 RSA 키로 TLS 해독 | SSLv2 완전 제거, 키 공유 서버 위험 |
| CCS Injection | CVE-2014-0224 | 2014 | ChangeCipherSpec 상태 머신 결함 | MITM 공격으로 약한 키 사용 강제 | 상태 머신 검증 강화 |
| Lucky Thirteen | CVE-2013-0169 | 2013 | CBC MAC 검증 타이밍 차이 | 타이밍 사이드 채널로 평문 복원 | 상수 시간 비교 필수, AEAD 선호 |
| 버퍼 오버플로 | CVE-2022-3602 | 2022 | X.509 이메일 주소 검증의 스택 오버플로 | 원격 코드 실행 가능 (제한적) | 인증서 파싱 코드 감사 강화 |
취약점 방어 체크리스트
# 1. OpenSSL 버전 확인 (알려진 취약점 없는 최신 버전)
openssl version
# OpenSSL 3.3.2 ... (Library: OpenSSL 3.3.2)
# 2. SSLv3/TLS 1.0/1.1 비활성화 확인
openssl s_client -connect example.com:443 -ssl3 2>&1 | grep -i error
openssl s_client -connect example.com:443 -tls1 2>&1 | grep -i error
# 둘 다 error 출력이어야 안전
# 3. CBC 모드 cipher 비활성화 확인 (Lucky Thirteen 방어)
openssl s_client -connect example.com:443 -cipher 'CBC' 2>&1 | grep -i error
# 4. CRIME 방어: 압축 비활성화
openssl s_client -connect example.com:443 -brief 2>&1 | grep Compression
# Compression: NONE
# 5. 인증서 만료/취약 키 점검
openssl s_client -connect example.com:443 2>/dev/null | \
openssl x509 -noout -dates -subject -issuer
# RSA 2048비트 이상, EC P-256 이상 확인
컨테이너와 클라우드 환경
컨테이너(Docker, Kubernetes)와 클라우드 네이티브 환경에서 OpenSSL은 TLS 통신의 핵심 인프라입니다. 인증서 자동화, 시크릿 관리, 사이드카 프록시 패턴이 결합되어 대규모 mTLS 인프라를 구성합니다.
컨테이너 이미지의 OpenSSL 관리
# 컨테이너 이미지별 OpenSSL 버전 확인
docker run --rm alpine openssl version
# OpenSSL 3.3.2 ... (Alpine은 최신 추적)
docker run --rm ubuntu:22.04 openssl version
# OpenSSL 3.0.2 ... (LTS, 보안 패치 백포트)
docker run --rm debian:bookworm openssl version
# OpenSSL 3.0.15 ...
# 컨테이너 내 취약 OpenSSL 스캔
trivy image --severity HIGH,CRITICAL myapp:latest 2>&1 | grep openssl
# libssl3 3.0.2-0ubuntu1.12 → 3.0.2-0ubuntu1.15 (CVE-2024-...)
# Distroless 이미지에서 OpenSSL (정적 링크)
# gcr.io/distroless/base — OpenSSL 동적 링크 포함
# gcr.io/distroless/static — OpenSSL 미포함 (Go/Rust 정적 빌드용)
인증서 자동 갱신과 무중단 교체
| 방식 | 도구 | OpenSSL 역할 | 갱신 주기 |
|---|---|---|---|
| cert-manager | Kubernetes CRD | 인증서 검증 (Init Container) | 자동 (만료 30일 전) |
| Vault PKI | HashiCorp Vault | CSR 생성, 인증서 검증 | 정책 기반 (TTL) |
| ACME (Let's Encrypt) | certbot, Lego | 키 생성, CSR, 인증서 설치 | 90일 (자동) |
| SPIFFE/SPIRE | Workload Identity | SVID X.509 인증서 | 1시간 (단기 인증서) |
커널 소스 분석: OpenSSL 연동 코드
리눅스 커널 내부에서 OpenSSL과 직접적으로 관련된 코드를 분석합니다. AF_ALG 소켓, kTLS 키 설정, 모듈 서명 검증의 커널 측 구현을 살펴봅니다.
AF_ALG 소켓 초기화 (crypto/af_alg.c)
/* crypto/af_alg.c — AF_ALG 소켓 주소 체계 등록 */
/* OpenSSL afalg ENGINE/Provider가 이 소켓을 통해 커널 Crypto API 호출 */
static const struct net_proto_family alg_family = {
.family = PF_ALG,
.create = alg_create,
.owner = THIS_MODULE,
};
static int __init af_alg_init(void)
{
int err = proto_register(&alg_proto, 0);
if (err)
goto out;
err = sock_register(&alg_family);
if (err != 0)
goto out_proto;
return 0;
out_proto:
proto_unregister(&alg_proto);
out:
return err;
}
/* bind() 시 알고리즘 유형과 이름을 파싱 */
static int alg_bind(struct socket *sock,
struct sockaddr *uaddr, int addr_len)
{
struct sockaddr_alg *sa = (void *)uaddr;
const struct af_alg_type *type;
/* salg_type: "skcipher", "hash", "aead", "rng" */
type = af_alg_type_lookup(sa->salg_type);
if (!type)
return -ENOENT;
/* salg_name: "cbc(aes)", "sha256", "gcm(aes)" 등 */
/* 커널 Crypto API에서 알고리즘 탐색 */
return type->bind(sa->salg_name, sa->salg_feat, sa->salg_mask);
}
kTLS 키 설정 (net/tls/tls_sw.c)
/* net/tls/tls_main.c — OpenSSL이 setsockopt()로 키를 전달하는 경로 */
/* SSL_CTX_set_options(ctx, SSL_OP_ENABLE_KTLS) 설정 후 */
/* OpenSSL은 핸드셰이크 완료 시 이 경로를 호출 */
static int do_tls_setsockopt_conf(struct sock *sk,
sockptr_t optval, unsigned int optlen, int tx)
{
struct tls_crypto_info *crypto_info;
struct tls_context *ctx = tls_get_ctx(sk);
/* OpenSSL이 전달한 구조체: 알고리즘, 키, IV, 시퀀스 번호 */
crypto_info = tx ? &ctx->crypto_send.info
: &ctx->crypto_recv.info;
copy_from_sockptr(crypto_info, optval, sizeof(*crypto_info));
/* 지원 cipher 확인: AES-GCM-128/256, ChaCha20-Poly1305 등 */
switch (crypto_info->cipher_type) {
case TLS_CIPHER_AES_GCM_128:
case TLS_CIPHER_AES_GCM_256:
case TLS_CIPHER_CHACHA20_POLY1305:
case TLS_CIPHER_AES_CCM_128:
break;
default:
return -EINVAL;
}
/* SW kTLS 또는 HW 오프로드 경로 선택 */
if (tx)
return tls_set_sw_offload(sk, ctx, tx);
else
return tls_set_sw_offload(sk, ctx, tx);
}
모듈 서명 도구 (scripts/sign-file.c)
/* scripts/sign-file.c — 커널 모듈 서명 (OpenSSL libcrypto 직접 사용) */
/* 빌드 시: sign-file sha512 certs/signing_key.pem ... module.ko */
#include <openssl/bio.h>
#include <openssl/evp.h>
#include <openssl/pem.h>
#include <openssl/pkcs7.h>
#include <openssl/err.h>
int main(int argc, char **argv)
{
/* argv: hash_algo, private_key, x509_cert, module */
const EVP_MD *digest = EVP_get_digestbyname(hash_algo);
/* BIO로 개인키/인증서 읽기 */
BIO *b = BIO_new_file(private_key_name, "r");
EVP_PKEY *private_key = PEM_read_bio_PrivateKey(b, NULL,
NULL, NULL);
b = BIO_new_file(x509_name, "r");
X509 *x509 = PEM_read_bio_X509(b, NULL, NULL, NULL);
/* 모듈 데이터를 BIO로 읽기 */
BIO *bm = BIO_new_file(module_name, "rb");
/* PKCS#7 서명 생성 (CMS detached signature) */
PKCS7 *pkcs7 = PKCS7_sign(x509, private_key, NULL, bm,
PKCS7_NOCERTS | PKCS7_BINARY |
PKCS7_DETACHED | PKCS7_NOATTR);
/* 서명을 모듈 파일 끝에 추가 */
i2d_PKCS7_bio(bm_out, pkcs7);
/* + module_signature 구조체 (magic, sig_len 등) */
}
sign-file은 OpenSSL의 BIO, EVP, PKCS7 API를
직접 사용하는 순수 C 프로그램입니다. 커널 빌드 시스템(scripts/Makefile)에서
-lcrypto로 libcrypto를 링크하며, OpenSSL이 설치되지 않으면 모듈 서명이 불가능합니다.
OpenSSL 트러블슈팅
OpenSSL 관련 문제를 진단하고 해결하는 실무 명령어와 패턴을 정리합니다.
에러 메시지 해석
# SSL 에러 코드 해석
openssl errstr 0A000126
# error:0A000126:SSL routines::unexpected eof while reading
# 상세 디버그 모드
openssl s_client -connect example.com:443 -debug -msg 2>&1 | head -50
# strace로 시스템 호출 추적
strace -e trace=network,read,write -f \
openssl s_client -connect example.com:443 2>&1 | grep -E "connect|read|write"
# 인증서 검증 실패 디버그
openssl verify -verbose -CAfile ca.crt -untrusted intermediate.crt server.crt
# error 20 at 0 depth lookup: unable to get local issuer certificate
# → CA 인증서 경로가 올바르지 않음
자주 발생하는 문제
| 증상 | 원인 | 해결 |
|---|---|---|
unable to get local issuer certificate |
중간 CA 인증서 누락 | SSL_CTX_load_verify_locations()에 체인 포함 |
certificate verify failed |
만료, 호스트명 불일치, 자체 서명 | 인증서 갱신, SAN 확인, CA 추가 |
no shared cipher |
서버/클라이언트 cipher suite 불일치 | openssl ciphers -v로 양측 확인 |
sslv3 alert handshake failure |
프로토콜 버전 불일치 또는 SNI 미설정 | -servername 옵션, 최소 버전 확인 |
SSL_ERROR_SYSCALL + errno=0 |
원격 측 연결 갑작스러운 종료 | 방화벽, 타임아웃, 서버 로그 확인 |
kTLS: TlsDecryptError 증가 |
잘못된 키/IV 전달 또는 시퀀스 불일치 | OpenSSL 버전 확인, SSL_OP_ENABLE_KTLS 재설정 |
FIPS: digital envelope routines::unsupported |
FIPS 모드에서 비승인 알고리즘 사용 | openssl list -providers로 활성 Provider 확인 |
성능 저하: openssl speed 값 낮음 |
AES-NI 비활성화 또는 OPENSSL_ia32cap 설정 | grep aes /proc/cpuinfo, 환경변수 확인 |
성능 프로파일링
# perf로 OpenSSL 핫스팟 분석
perf record -g openssl speed -evp aes-256-gcm -seconds 5
perf report --no-children
# 예상 출력 (AES-NI 활성 시)
# 45.2% openssl libcrypto.so [.] aesni_ctr32_ghash_6x
# 12.1% openssl libcrypto.so [.] gcm_ghash_avx
# 8.3% openssl [kernel] [k] copy_user_enhanced_fast_string
# BPF로 TLS 핸드셰이크 지연 측정
bpftrace -e 'uprobe:/usr/lib64/libssl.so.3:SSL_do_handshake {
@start[tid] = nsecs;
}
uretprobe:/usr/lib64/libssl.so.3:SSL_do_handshake /@start[tid]/ {
@handshake_us = hist((nsecs - @start[tid]) / 1000);
delete(@start[tid]);
}'
OpenSSL 1.x → 3.x 마이그레이션 가이드
OpenSSL 1.1.1은 2023년 9월에 EOL(End of Life)이 되었습니다. 3.x로의 마이그레이션은 API 변경, deprecated 함수 교체, Provider 전환을 포함합니다.
주요 API 변경
| 1.x API | 3.x 대체 API | 변경 이유 |
|---|---|---|
EVP_MD_CTX_create() | EVP_MD_CTX_new() | 이름 일관성 |
EVP_MD_CTX_destroy() | EVP_MD_CTX_free() | 이름 일관성 |
EVP_sha256() | EVP_MD_fetch(ctx, "SHA2-256", prop) | Provider 지원 |
EVP_aes_256_gcm() | EVP_CIPHER_fetch(ctx, "AES-256-GCM", prop) | Provider 지원 |
RSA_generate_key_ex() | EVP_PKEY_keygen() | 저수준 API deprecated |
RSA_sign() | EVP_DigestSign() | EVP 통합 |
ENGINE_by_id() | OSSL_PROVIDER_load() | Provider 전환 |
RAND_bytes() | RAND_bytes() (유지) | 변경 없음 |
SSL_CTX_set_cipher_list() | SSL_CTX_set_cipher_list() + SSL_CTX_set_ciphersuites() | TLS 1.3 분리 |
/* 마이그레이션 예제: RSA 키 생성 */
/* ❌ 1.x 방식 (deprecated) */
RSA *rsa = RSA_new();
BIGNUM *e = BN_new();
BN_set_word(e, RSA_F4);
RSA_generate_key_ex(rsa, 2048, e, NULL);
/* ✅ 3.x 방식 (권장) */
EVP_PKEY_CTX *pctx = EVP_PKEY_CTX_new_from_name(NULL, "RSA", NULL);
EVP_PKEY_keygen_init(pctx);
EVP_PKEY_CTX_set_rsa_keygen_bits(pctx, 2048);
EVP_PKEY *pkey = NULL;
EVP_PKEY_keygen(pctx, &pkey);
EVP_PKEY_CTX_free(pctx);
# deprecated 사용 감지 (빌드 시 경고)
gcc -DOPENSSL_API_COMPAT=30000 -Wall myapp.c -lcrypto -lssl
# deprecated 함수 사용 시 컴파일 경고 발생
# 런타임 deprecated 로그
OPENSSL_DEBUG_MEMORY=on OPENSSL_CONF=/dev/null ./myapp 2>&1 | grep -i deprec
관련 문서
OpenSSL과 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.
외부 참고 자료
- OpenSSL Documentation — OpenSSL 공식 문서 인덱스입니다
- OpenSSL 3.x Provider — OpenSSL 3.x Provider 아키텍처 공식 문서입니다
- OpenSSL crypto(7) — OpenSSL 암호화 라이브러리 개요입니다
- OpenSSL ssl(7) — OpenSSL TLS/SSL 라이브러리 개요입니다
- OpenSSL FIPS Module — OpenSSL 3.x FIPS 140-2 모듈 설명입니다
- OpenSSL Default Provider — 기본 Provider의 알고리즘 목록입니다
- OpenSSL GitHub Repository — OpenSSL 소스 코드 공식 저장소입니다
- OpenSSL Wiki — OpenSSL 커뮤니티 위키입니다
- openssl(1) — Command Line Tool — OpenSSL 명령줄 도구 매뉴얼입니다
- OpenSSL Changelog — OpenSSL 버전별 변경 사항 및 보안 수정 내역입니다
- OpenSSL Vulnerabilities — OpenSSL 보안 취약점 공식 목록입니다
- RFC 8446 — TLS 1.3 — TLS 1.3 프로토콜 사양입니다