OpenSSL

전제 조건: Linux Crypto Framework (Crypto API) 문서를 먼저 읽으면 커널 측 암호화 개념을 이해하는 데 도움이 됩니다. kTLS 문서는 커널 TLS 오프로드 상세를 다룹니다.
일상 비유: OpenSSL은 "암호화 만능 도구 상자"와 같습니다. 일반 자물쇠(대칭 암호), 금고 열쇠(비대칭 암호), 봉인 도장(인증서), 비밀 편지 봉투(TLS)를 모두 꺼내 쓸 수 있는 도구 상자이며, 리눅스 커널은 이 도구 상자가 더 빠르고 안전하게 동작할 수 있도록 하드웨어 가속과 커널 수준 보호를 제공합니다.

핵심 요약

  • 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 발급, 서명, 검증의 전체 생명주기를 담당합니다.

단계별 이해

  1. OpenSSL 계층 이해
    Application → libssl(TLS 프로토콜) → libcrypto(암호 연산) → Provider(구현체) 구조를 파악합니다.
  2. 커널 연동 포인트 파악
    OpenSSL이 리눅스 커널과 상호작용하는 4가지 경로(AF_ALG, kTLS, /dev/urandom, 모듈 서명)를 이해합니다.
  3. Provider 시스템 학습
    Default, FIPS, Legacy, PKCS#11 프로바이더의 역할과 설정 방법을 익힙니다.
  4. 보안 설정 적용
    TLS 프로토콜 버전 제한, cipher suite 선택, openssl.cnf 강화 방법을 적용합니다.
  5. 성능 최적화
    하드웨어 가속 활용, 비동기 연산, openssl speed 벤치마크 해석으로 최적 구성을 찾습니다.

OpenSSL 3.x 아키텍처

OpenSSL 3.x는 1.x 시대의 모놀리식 구조를 탈피하여 프로바이더(Provider) 기반 모듈형 아키텍처로 전환되었습니다. 애플리케이션은 libssl(TLS 프로토콜 엔진)과 libcrypto(암호 연산 라이브러리)를 통해 암호화 기능을 사용하며, 실제 암호화 구현은 프로바이더라는 독립된 모듈이 담당합니다.

libssl vs libcrypto 역할 분리

계층라이브러리역할주요 API
TLS 프로토콜libsslTLS/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()
애플리케이션 (nginx, HAProxy, curl, Node.js, Python) libssl — TLS/DTLS 프로토콜 엔진 libcrypto — 암호 연산 라이브러리 EVP API (고수준 통합 인터페이스) CORE — Provider 디스패치 & 알고리즘 탐색 Default Provider AES, SHA, RSA, EC (범용 SW 구현) FIPS Provider FIPS 140-3 검증 (자체 무결성 테스트) Legacy Provider MD5, DES, RC4 (deprecated 알고리즘) Third-Party Provider PKCS#11, QAT, AF_ALG (HW 가속 / HSM 연동) 하드웨어: AES-NI / AVX-512 / QAT / HSM / TPM / 커널 Crypto API (AF_ALG)
OpenSSL 3.x는 CORE 디스패처를 통해 다수의 Provider를 동적으로 로딩하여 알고리즘을 선택합니다.

ENGINE에서 Provider로의 전환

OpenSSL 1.x에서 하드웨어 가속이나 외부 키 저장소를 사용하려면 ENGINE API를 통해 커스텀 구현을 등록했습니다. OpenSSL 3.x에서 ENGINE은 deprecated 상태이며, 동일한 기능을 Provider가 대체합니다.

ENGINE의 구조적 문제점

ENGINE API가 deprecated된 것은 단순한 API 정리가 아니라, 근본적인 아키텍처 결함을 해결하기 위한 것입니다. OpenSSL 프로젝트가 공식적으로 밝힌 전환 이유와 기술적 배경을 분석합니다.

ENGINE 아키텍처 (1.x) — 문제점 애플리케이션 libcrypto 내부 글로벌 ENGINE 테이블 (싱글턴, 락 경합) ENGINE "pkcs11" ENGINE "afalg" ENGINE "qat" ❌ 저수준 API 직접 접근 (RSA_METHOD, EC_KEY_METHOD) ❌ libcrypto 내부 구조체에 의존 → ABI 깨짐 ENGINE의 구조적 문제 ① 글로벌 상태 — 프로세스당 단일 ENGINE 테이블 ② 락 경합 — 멀티스레드에서 등록/해제 시 글로벌 락 ③ ABI 깨짐 — RSA_METHOD 등 내부 구조체 직접 참조 ④ FIPS 불가 — ENGINE은 FIPS 모듈 경계 밖 ⑤ 알고리즘 발견 불가 — ID 기반 명시적 선택만 ⑥ 테넌트 격리 불가 — 컨텍스트별 ENGINE 불가 ⑦ 에러 처리 불투명 — ENGINE 내부 에러 전파 어려움 ⑧ 부분 교체 불가 — all-or-nothing 방식 ⑨ 메모리 관리 — ENGINE과 libcrypto 할당기 불일치 Provider 아키텍처 (3.x) — 해결 애플리케이션 EVP API + CORE 디스패처 (안정 ABI) OSSL_DISPATCH 함수 테이블 (ABI 경계) Default Prov. FIPS Prov. Custom Prov. Provider가 해결한 문제 ✓ OSSL_LIB_CTX별 독립 Provider 세트 ✓ 컨텍스트별 알고리즘 캐시 → 락 경합 최소 ✓ OSSL_DISPATCH 테이블 = 안정 ABI 경계 ✓ FIPS Provider = 자체 완결 암호 모듈 ✓ property query로 알고리즘 자동 발견 ✓ 테넌트별 Provider 격리 (클라우드 환경) ✓ OSSL_PARAM으로 통일된 에러/파라미터 전달 ✓ 알고리즘 단위 세밀 교체 가능 ✓ CORE 할당기 사용 → 메모리 일관성
ENGINE은 libcrypto 내부 구조체에 직접 의존하는 글로벌 싱글턴 방식이었으나, Provider는 안정된 ABI 경계(OSSL_DISPATCH)와 컨텍스트별 격리를 제공합니다.

핵심 문제: 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);
타임라인: ENGINE API는 OpenSSL 3.0에서 deprecated, 3.x 기간 동안 호환 레이어로 유지됩니다. OpenSSL 4.0(향후)에서 완전 제거가 예정되어 있으므로, 신규 프로젝트에서는 반드시 Provider를 사용해야 합니다. 기존 ENGINE 코드는 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=DIRlib 또는 lib64라이브러리 설치 디렉터리명
--banner=TEXTopenssl 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-picPIC(Position Independent Code) 비활성화정적 전용 빌드 시 성능 최적화
no-asm어셈블리 최적화 비활성화 (순수 C)미지원 아키텍처, 디버깅
no-appsopenssl CLI 도구 빌드 제외라이브러리만 필요한 임베디드
no-docsman 페이지 빌드/설치 제외최소 설치
no-tests테스트 바이너리 빌드 제외CI에서 빌드만 검증 시
no-module동적 Provider .so 빌드 비활성화정적 Provider만 사용

프로토콜 옵션

옵션효과보안/성능 영향
no-ssl3SSLv3 비활성화POODLE 공격 방지 (기본 비활성화)
no-tls1TLS 1.0 비활성화BEAST, Lucky13 방지
no-tls1_1TLS 1.1 비활성화약한 cipher 제거
no-tls1_2TLS 1.2 비활성화TLS 1.3 전용 환경
no-tls1_3TLS 1.3 비활성화레거시 호환 필요 시만
no-dtlsDTLS 전체 비활성화UDP 기반 TLS 불필요 시
no-dtls1DTLS 1.0 비활성화보안 강화
no-dtls1_2DTLS 1.2 비활성화
enable-ktlskTLS 오프로드 활성화Linux 4.13+에서 성능 향상
enable-quicQUIC 프로토콜 API 활성화HTTP/3 지원 (3.2+)
no-nextprotonegNPN 확장 비활성화ALPN만 사용 (NPN은 deprecated)
no-srpSRP(Secure Remote Password) 비활성화사용처 거의 없음
no-srtpSRTP(Secure RTP) 확장 비활성화WebRTC 미사용 시
no-pskPSK(Pre-Shared Key) TLS 비활성화PSK 미사용 시
no-compTLS 압축 비활성화CRIME 공격 방지
no-ocspOCSP 지원 비활성화CRL만 사용 시
no-ctCertificate Transparency 비활성화CT 검증 불필요 시
no-cmsCMS(Cryptographic Message Syntax) 비활성화S/MIME 불필요 시
no-ts타임스탬프 프로토콜(TSP) 비활성화TSA 불필요 시

알고리즘 옵션

옵션비활성화 대상보안 영향
대칭 암호
no-desDES, 3DESSweet32 공격 방지 (64비트 블록)
no-rc2RC2취약 알고리즘 제거
no-rc4RC4RC4 바이어스 공격 방지
no-rc5RC5특허 만료, 사용처 없음
no-ideaIDEA사용처 거의 없음
no-seedSEED (한국 표준)한국 외 사용처 없음
no-bfBlowfish64비트 블록, 레거시
no-castCAST5사용처 거의 없음
no-camelliaCamellia일본 표준, AES 대안
no-ariaARIA (한국 표준)한국 외 사용처 제한적
no-sm4SM4 (중국 표준)중국 외 사용처 없음
no-chachaChaCha20-Poly1305모바일 환경에서 AES-NI 없을 때 유용
해시
no-md2MD2완전히 깨진 해시 (기본 비활성화)
no-md4MD4완전히 깨진 해시
no-md5MD5충돌 공격 가능 (HMAC은 안전)
no-rmd160RIPEMD-160비트코인 외 사용처 제한적
no-whirlpoolWhirlpool사용처 거의 없음
no-sm3SM3 (중국 표준)중국 외 사용처 없음
비대칭
no-dsaDSAEdDSA/ECDSA로 대체
no-dhDH(Diffie-Hellman)ECDH로 대체 가능
no-ec타원 곡선 전체ECDSA/ECDH/Ed25519 모두 비활성화
no-ec2mGF(2^m) 타원 곡선바이너리 곡선 제거 (GF(p)만 유지)
no-sm2SM2 (중국 표준)중국 외 사용처 없음
기타
no-gostGOST (러시아 표준)러시아 외 사용처 없음
no-sivAES-SIV (RFC 5297)nonce 오용 방지 모드
no-poly1305Poly1305 MACChaCha20-Poly1305에 필요

Provider 및 ENGINE 옵션

옵션효과사용 상황
enable-fipsFIPS Provider 빌드FIPS 140-3 규제 환경 (금융, 정부)
no-legacyLegacy Provider 빌드 제외보안 강화 (취약 알고리즘 완전 제거)
no-engineENGINE 서브시스템 전체 제거Provider만 사용, ENGINE 의존성 감지
no-deprecateddeprecated API 컴파일 제외새 프로젝트, 1.x 코드 미사용 확인
no-module동적 Provider .so 빌드 비활성화정적 Provider 내장
no-autoload-configopenssl.cnf 자동 로딩 비활성화Provider를 코드로만 로드

보안 및 디버그 옵션

옵션효과사용 상황
--debug디버그 심볼 + 최적화 해제 (-g -O0)개발/디버깅
--release릴리스 최적화 (-O3)프로덕션 빌드
enable-asanAddressSanitizer 활성화힙/스택 오버플로 탐지
enable-ubsanUndefinedBehaviorSanitizer정의되지 않은 동작 탐지
enable-msanMemorySanitizer (Clang 전용)초기화되지 않은 메모리 읽기 탐지
enable-tsanThreadSanitizer데이터 레이스 탐지
enable-fuzz-libfuzzerlibFuzzer 퍼징 빌드보안 테스팅
enable-fuzz-aflAFL 퍼징 빌드보안 테스팅
no-secure-memorymlock() 기반 보안 메모리 비활성화mlock 권한 없는 환경
-DPURIFYValgrind 호환 모드메모리 분석 도구 사용 시
no-threads멀티스레딩 지원 비활성화단일 스레드 임베디드
no-async비동기 연산 비활성화QAT 등 비동기 Provider 미사용

플랫폼 및 최적화 옵션

옵션효과사용 상황
no-asm어셈블리 최적화 비활성화 (순수 C)미지원 아키텍처, 디버깅, ASAN
386x86 386 호환 코드 생성레거시 x86 (SSE2 미지원)
no-sse2SSE2 사용 비활성화매우 오래된 x86
enable-ec_nistp_64_gcc_128P-256/P-521 고속 구현 (GCC __int128)x86_64 GCC 빌드 성능 향상
no-hw하드웨어 가속 비활성화순수 SW 테스트, FIPS 기준선
no-afalgengAF_ALG ENGINE 비활성화AF_ALG 미사용
no-padlockengVIA PadLock ENGINE 비활성화VIA CPU 미사용
enable-zlibzlib 기반 압축 활성화CMS/PKCS7 압축 (TLS 압축은 별도)
enable-brotliBrotli 인증서 압축 (RFC 8879)TLS 인증서 크기 절감
enable-zstdZstandard 인증서 압축 (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_write() BIO_f_buffer 버퍼링 필터 BIO_f_ssl TLS 암호화 필터 BIO_s_socket TCP 소켓 싱크 네트워크 소스/싱크 BIO (Source/Sink) BIO_s_socket — TCP 소켓 BIO_s_file — 파일 I/O BIO_s_mem — 메모리 버퍼 BIO_s_connect — 클라이언트 BIO_s_accept — 서버 리스닝 BIO_s_datagram — UDP BIO_s_bio — BIO 쌍 (파이프) BIO_s_null — /dev/null 필터 BIO (Filter) BIO_f_ssl — TLS 암/복호화 BIO_f_buffer — 버퍼링 BIO_f_base64 — Base64 BIO_f_cipher — 암호화 BIO_f_md — 해시 계산 BIO_f_zlib — 압축 BIO_f_readbuffer — 읽기버퍼 BIO_f_prefix — 접두사
BIO는 소스/싱크(데이터 원본/목적지)와 필터(데이터 변환)로 나뉘며, 체인으로 연결하여 복합 I/O 파이프라인을 구성합니다.
/* 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
MACHMAC, CMAC, GMAC, Poly1305, KMAC
비대칭RSA (PKCS#1, PSS, OAEP), EC (NIST P-256/384/521, secp256k1), Ed25519/Ed448, X25519/X448, SM2
KDFHKDF, 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에서 금지)
주의: 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
openssl.cnf [provider_sect] 파싱 OSSL_PROVIDER_load() .so 모듈 동적 로딩 OSSL_provider_init() 알고리즘 테이블 등록 EVP_MD_fetch("SHA256", ...) property query로 알고리즘 요청 CORE 디스패치 property 매칭 검색 매칭된 Provider 구현 함수 포인터 반환 Property Query 예시 provider=default → Default Provider의 구현만 선택 fips=yes → FIPS 인증 알고리즘만 선택 provider=pkcs11 → HSM에서 실행되는 구현만 선택 알고리즘 등록 완료 ① 초기화 단계 ② 런타임 알고리즘 요청
Provider는 초기화 시 알고리즘을 등록하고, 런타임에 property query를 통해 최적의 구현이 선택됩니다.

OSSL_LIB_CTX 멀티 컨텍스트 격리

OpenSSL 3.x는 OSSL_LIB_CTX를 통해 라이브러리 전역 상태를 컨텍스트별로 격리할 수 있습니다. 이는 동일 프로세스 내에서 FIPS Provider와 Default Provider를 독립적으로 운용하거나, 멀티테넌트 애플리케이션에서 테넌트별 암호화 설정을 분리하는 데 사용됩니다.

애플리케이션 프로세스 기본 컨텍스트 (NULL) Default Provider Legacy Provider (선택) EVP 알고리즘 캐시 DRBG (난수 상태) ERR 큐 · 설정 FIPS 전용 컨텍스트 FIPS Provider만 로드 FIPS 알고리즘만 캐시 독립 DRBG 인스턴스 독립 ERR 큐 · 설정 자체 무결성 검증 통과 HSM 전용 컨텍스트 PKCS#11 Provider HSM 세션 관리 키: HSM 내부만 독립 알고리즘 캐시 NULL (기본) fips_ctx hsm_ctx
OSSL_LIB_CTX를 사용하면 동일 프로세스에서 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);
활용 시나리오: 금융 서비스에서 외부 API 통신은 FIPS 컨텍스트로, 내부 로그 암호화는 Default 컨텍스트로 분리하면 FIPS 규제 준수와 성능을 동시에 확보할 수 있습니다.

OpenSSL과 커널 연동 포인트

OpenSSL은 순수 사용자 공간(User Space) 라이브러리이지만, 리눅스 커널과 4가지 핵심 채널을 통해 상호작용합니다. 이 연동을 이해하면 성능 최적화와 보안 강화의 핵심 의사결정을 내릴 수 있습니다.

사용자 공간 (User Space) OpenSSL (libssl + libcrypto) nginx / HAProxy / curl sign-file / 사용자 도구 ① AF_ALG 소켓 커널 Crypto API 호출 ② kTLS 오프로드 TLS 데이터 경로 ③ /dev/urandom 난수 시드 획득 ④ 모듈 서명 sign-file 키 생성 커널 공간 (Kernel Space) Crypto API af_alg.ko, algif_*.ko kTLS (tls.ko) TLS 레코드 처리 CRNG (ChaCha20) getrandom() 시스콜 모듈 서명 검증 PKCS#7 / X.509 하드웨어 AES-NI / SHA-NI / QAT / NIC TLS 오프로드 / TRNG / TPM socket(AF_ALG) setsockopt(TLS) getrandom() finit_module()
OpenSSL은 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 */
실무 판단: AES-NI가 있는 x86 서버에서는 OpenSSL의 사용자 공간 구현이 AF_ALG보다 빠릅니다. AF_ALG는 커널에 등록된 전용 하드웨어 가속기(QAT, CAAM 등)가 있을 때 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 하드웨어 오프로드까지 연결할 수 있습니다.

기존 방식 (SW TLS) kTLS 오프로드 애플리케이션 (nginx) OpenSSL 핸드셰이크 + 데이터 암호화 커널 TCP 소켓 NIC (평문 TCP 전송) 평문 데이터 암호문 (복사) 암호문 (복사) 성능 병목 • User↔Kernel 데이터 복사 2회 • sendfile() 사용 불가 • 암호화가 CPU에서 수행 애플리케이션 (nginx) OpenSSL 핸드셰이크만 (키 협상) kTLS (tls.ko) — 암호화 처리 NIC (HW TLS 오프로드 가능) 평문 데이터 키 전달 (1회) sendfile() 가능 성능 이점 • 제로 카피 sendfile() 지원 • NIC HW TLS 오프로드 연계 • CPU 사용률 30-50% 감소
기존 SW TLS는 모든 암호화를 사용자 공간에서 수행하지만, kTLS는 데이터 경로를 커널에 위임하여 제로 카피와 HW 오프로드를 가능하게 합니다.
/* 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;
kTLS 상세: kTLS의 아키텍처, 지원 암호 스위트, NIC 오프로드 설정에 대한 자세한 내용은 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~12AES-NI 미지원
ARM CE (Cortex-A76+)~4 GB/s~1.0ARM 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)CPUQAT 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 방식
QAT 상세: QAT 드라이버 설치, 커널 모듈 구성, 압축 가속 등 상세 내용은 Crypto API의 QAT 섹션을 참고하세요.

TLS 1.3 핸드셰이크 상세

TLS 1.3(RFC 8446)은 핸드셰이크를 1-RTT(Round-Trip Time)로 단축하고, 모든 핸드셰이크 메시지를 가능한 빨리 암호화하여 메타데이터 노출을 최소화합니다. OpenSSL은 ssl/statem/ 디렉터리의 상태 머신으로 이 프로토콜을 구현합니다.

클라이언트 (OpenSSL) 서버 (OpenSSL) ① ClientHello 지원 cipher, key_share(X25519), SNI ② ServerHello 선택된 cipher, key_share(서버) 핸드셰이크 키 파생 핸드셰이크 키 파생 ── 이후 모든 메시지 암호화 ── ③ EncryptedExtensions ④ Certificate (서버 인증서) ⑤ CertificateVerify (서명) ⑥ Finished (MAC 검증) 애플리케이션 키 파생 ⑦ Finished (클라이언트) 애플리케이션 키 파생 ── 애플리케이션 데이터 전송 (1-RTT 완료) ── HTTP 요청/응답 (암호화) 0-RTT (Early Data) — 세션 재개 시 이전 세션의 PSK(Pre-Shared Key)로 ClientHello와 함께 애플리케이션 데이터 즉시 전송 SSL_CTX_set_max_early_data() · SSL_write_early_data() · SSL_read_early_data() ⚠ 재전송 공격(Replay Attack) 위험 — 멱등성 보장 필요
TLS 1.3은 1-RTT 핸드셰이크로 ServerHello 직후부터 암호화를 시작하며, 세션 재개 시 0-RTT로 즉시 데이터를 전송할 수 있습니다.

TLS 1.2 vs 1.3 핵심 차이

항목TLS 1.2TLS 1.3
핸드셰이크2-RTT1-RTT (0-RTT 재개)
키 교환RSA, DHE, ECDHEECDHE, DHE만 (RSA 키 교환 제거)
암호 스위트수십 개 조합 가능5개만 (AEAD 필수)
핸드셰이크 암호화Finished만 암호화ServerHello 이후 전체 암호화
PFS선택 (ECDHE 사용 시)필수 (항상 임시 키)
압축지원 (CRIME 취약)제거
재협상지원제거 (KeyUpdate로 대체)
세션 재개Session ID / TicketPSK (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;
}
0-RTT 보안 주의: Early Data는 재전송 공격에 취약합니다. 공격자가 캡처한 0-RTT 데이터를 재전송하면 서버가 중복 처리할 수 있습니다. GET 요청 등 멱등성(idempotent)이 보장된 요청에만 0-RTT를 허용하고, POST/PUT 등 상태 변경 요청은 반드시 1-RTT 이후 처리해야 합니다.

mTLS (상호 TLS 인증)

mTLS(Mutual TLS)는 서버뿐 아니라 클라이언트도 인증서를 제출하여 양방향으로 신원을 검증하는 TLS 설정입니다. 마이크로서비스 간 통신, API 게이트웨이, 제로 트러스트(Zero Trust) 네트워크에서 핵심적으로 사용됩니다.

클라이언트 클라이언트 인증서 + 개인키 서버 CA 인증서 (Trust Store) 서버 서버 인증서 + 개인키 클라이언트 CA 인증서 (검증용) ① ClientHello ② ServerHello + 서버 인증서 ③ CertificateRequest (클라이언트 인증서 요청) ④ 클라이언트 인증서 + CertificateVerify ⑤ Finished ⑥ Finished
mTLS에서 서버는 CertificateRequest 메시지로 클라이언트 인증서를 요구하고, 클라이언트는 자신의 인증서와 서명으로 신원을 증명합니다.
/* 서버: 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 활용 시나리오

시나리오설명도구/프레임워크
서비스 메시마이크로서비스 간 자동 mTLSIstio, 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이 활용됩니다.

인증서 체인 검증 흐름

Root CA 인증서 자체 서명 (Self-Signed) Intermediate CA 인증서 Root CA가 서명 서버 인증서 (End Entity) Intermediate CA가 서명 서명 발급 서명 발급 검증 경로 (하→상) ① 서버 인증서 서명 확인 ② Intermediate 서명 확인 ③ Root CA: Trust Anchor 도달 ④ 유효기간, CRL/OCSP 확인 Trust Store /etc/ssl/certs/ca-certificates.crt /etc/pki/tls/certs/ca-bundle.crt 신뢰할 Root CA 목록 매칭 폐기 확인 CRL (인증서 폐기 목록) OCSP (온라인 상태 프로토콜) 커널에서의 X.509 활용 모듈 서명 검증 | IMA 정책 | Secure Boot (MOK) | kTLS 인증서
인증서 체인은 End Entity → Intermediate CA → Root CA 순으로 서명을 검증하며, Trust Store의 Root CA에 도달하면 신뢰가 확립됩니다.

인증서 실무 명령어

# 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.3TLS_AES_256_GCM_SHA384
TLS_CHACHA20_POLY1305_SHA256
TLS_AES_128_GCM_SHA256
모두 AEAD, PFS 기본 보장
TLS 1.2 (권장)ECDHE-ECDSA-AES256-GCM-SHA384
ECDHE-RSA-AES256-GCM-SHA384
ECDHE-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.3QUIC (UDP + 내장 TLS)
연결 수립TCP 3-way + TLS 1-RTT = 2-RTT1-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 3.2는 QUIC 클라이언트를, 3.4부터 서버도 지원합니다. nginx의 QUIC 지원은 BoringSSL 기반이 주류였으나, OpenSSL QUIC API 안정화에 따라 OpenSSL 기반 QUIC 지원도 확대되고 있습니다. 커널 수준 QUIC 오프로드는 아직 초기 단계이며, kTLS처럼 데이터 경로를 커널에 위임하는 구조가 논의 중입니다.

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, straceftrace, bpftrace, crash
실무 가이드라인: 대부분의 사용자 공간 애플리케이션에서는 OpenSSL을 사용하는 것이 올바른 선택입니다. 커널 Crypto API는 커널 서브시스템(IPsec, dm-crypt, kTLS, NFS/SMB 암호화)이 내부적으로 사용하며, 사용자가 직접 AF_ALG를 호출하는 것은 전용 HW 가속기가 있을 때만 이점이 있습니다.

구현 시 자주 틀리는 지점

실수문제올바른 방법
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를 통해야 합니다.

EVP API 객체 계층과 연산 흐름 Fetch 계층 (알고리즘 탐색 — Provider 디스패치) EVP_MD_fetch() EVP_CIPHER_fetch() EVP_MAC_fetch() EVP_KDF_fetch() EVP_MD 해시 알고리즘 디스크립터 EVP_CIPHER 대칭 암호 디스크립터 EVP_MAC MAC 알고리즘 디스크립터 EVP_KDF 키 파생 함수 디스크립터 EVP_MD_CTX 해시 연산 컨텍스트 (상태) EVP_CIPHER_CTX 암호화 연산 컨텍스트 (상태) EVP_MAC_CTX MAC 연산 컨텍스트 EVP_KDF_CTX 키 파생 컨텍스트 연산 패턴: Init → Update(반복) → Final Init(ctx, alg, key) Update(ctx, data, len) ×N Final(ctx, output) free(ctx) EVP_PKEY — 비대칭 키 컨테이너 RSA, EC, Ed25519, X25519, DH 키를 통합 래핑 EVP_PKEY_CTX keygen / sign verify / derive encrypt / decrypt / encapsulate (KEM) SSL / SSL_CTX — TLS 연결 관리 libssl이 libcrypto EVP를 내부적으로 사용 SSL_CTX SSL BIO X509 handshake / read / write / shutdown
EVP API는 Fetch(알고리즘 탐색) → 디스크립터(불변) → 컨텍스트(상태) 3단계로 구성되며, Init→Update→Final 패턴으로 모든 연산을 수행합니다.

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()으로 재사용할 수 있습니다.

new() Init() cipher + key + iv Update() data 처리 (반복 가능) Final() 패딩 + 마지막 블록 free() 반복 reset() → 재사용 (키/IV 변경) GCM/CCM 모드 추가 단계 Init → ctrl(SET_IVLEN) → Init(key,iv) → Update(AAD) → Update(data) → Final → ctrl(GET_TAG)
EVP_CIPHER_CTX는 Init → Update(반복) → Final 상태 머신을 따르며, 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 차이패딩인증병렬화용도
GCMctrl(SET_IVLEN), ctrl(GET_TAG), AAD 별도불필요 (스트림)GMAC 태그암호화 가능TLS 1.2/1.3, IPsec
CCMctrl(SET_IVLEN), ctrl(SET_TAG) 필수, 길이 사전 지정불필요CBC-MAC 태그불가IEEE 802.15.4, BLE
CBC패딩 주의 (PKCS#7 기본)필요 (마지막 블록)없음복호화만레거시 호환
CTR패딩 불필요, 카운터 관리불필요 (스트림)없음암/복호화디스크 암호화
XTS키 2배 (256→512비트), tweak valueciphertext stealing없음블록 독립디스크 섹터 암호화
WRAPAES 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를 통해 수행합니다.

키 생성 EVP_PKEY_CTX_new_from_name() EVP_PKEY_keygen_init() EVP_PKEY_keygen() → EVP_PKEY * 또는 PEM/DER 파일에서 로드 EVP_PKEY RSA | EC | Ed25519 | X25519 | DH 직렬화 / 내보내기 PEM_write_bio_PrivateKey() PEM_write_bio_PUBKEY() i2d_PrivateKey() (DER) EVP_PKEY_todata() (OSSL_PARAM) 서명 / 검증 EVP_DigestSign*() EVP_DigestVerify*() RSA-PSS, ECDSA, Ed25519 암호화 / 복호화 EVP_PKEY_encrypt() EVP_PKEY_decrypt() RSA-OAEP 키 교환 / 파생 EVP_PKEY_derive() ECDH, X25519, DH → 공유 비밀 생성 KEM (캡슐화) EVP_PKEY_encapsulate() EVP_PKEY_decapsulate() PQC (양자 내성) 알고리즘별 지원 연산 알고리즘 서명 암호화 키교환 KEM 키 크기 RSA ✓ PSS/PKCS1 ✓ OAEP 2048/3072/4096 EC (P-256) ✓ ECDSA ✓ ECDH 256/384/521 bit
EVP_PKEY는 키 생성 → 연산(서명/암호화/키교환/KEM) → 직렬화의 전체 생명주기를 관리하며, 알고리즘에 독립적인 코드를 가능하게 합니다.

키 생성 상세

/* 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_CTXSSL은 libssl의 핵심 객체입니다. SSL_CTX는 TLS 설정의 공장(Factory)으로 프로세스 수명 동안 유지되며, SSL은 개별 연결의 상태 머신으로 연결당 하나씩 생성됩니다.

SSL_CTX (공유, 프로세스 수명) 인증서 + 개인키 (SSL_CTX_use_certificate/PrivateKey) CA 체인 (SSL_CTX_load_verify_locations) Cipher Suite 설정 (set_cipher_list / set_ciphersuites) 프로토콜 버전 (set_min/max_proto_version) 검증 설정 (SSL_CTX_set_verify) 세션 캐시 (SSL_CTX_set_session_cache_mode) ALPN 설정 (SSL_CTX_set_alpn_protos) 콜백 함수들 옵션 플래그 참조 카운팅 — SSL_CTX_up_ref() / SSL_CTX_free() SSL #1 (연결 A) 상태: HANDSHAKE BIO: socket fd=5 세션: 신규 SNI: api.example.com 호스트명 검증 설정 SSL_new() → SSL_free() SSL #2 (연결 B) 상태: DATA BIO: socket fd=8 세션: 재사용 (PSK) SNI: www.example.com kTLS 활성 SSL_new() → SSL_free() SSL_new(ctx) SSL 연결 상태 머신 INIT HANDSHAKE DATA SHUTDOWN CLOSED connect/accept 완료 shutdown 주요 API 호출 SSL_connect() / SSL_accept() SSL_read() / SSL_write() SSL_shutdown() (2회 호출) SSL_get_error() — 비블로킹 상태 확인
SSL_CTX는 TLS 설정의 공장으로 다수의 SSL 연결에 공유되며, 각 SSL 객체는 INIT→HANDSHAKE→DATA→SHUTDOWN→CLOSED 상태 머신을 따릅니다.

비블로킹(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_callbackSSL_CTX_set_verify()인증서 체인 검증 시 각 인증서마다커스텀 검증 로직 (핀닝 등)
info_callbackSSL_CTX_set_info_callback()상태 전환마다디버깅, 로깅, 모니터링
msg_callbackSSL_CTX_set_msg_callback()TLS 메시지 송수신 시프로토콜 분석, 키로깅
alpn_select_cbSSL_CTX_set_alpn_select_cb()핸드셰이크 중 ALPN 협상h2, http/1.1 프로토콜 선택
servername_cbSSL_CTX_set_tlsext_servername_callback()SNI 확장 수신 시가상 호스팅, SSL_CTX 전환
keylog_callbackSSL_CTX_set_keylog_callback()키 교환 완료 시Wireshark 디코딩용 키 로깅
ticket_key_cbSSL_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()으로 로딩합니다.

OpenSSL CORE (libcrypto) OSSL_PROVIDER_load(ctx, "myprov") CORE → Provider 전달 함수 (in) core_get_params, core_new_error, core_obj_create ... Provider → CORE 반환 함수 (out) query_operation, teardown, get_params, get_reason_strings 알고리즘 캐시 EVP_MD_fetch() EVP_CIPHER_fetch() → 캐시에서 검색 Property 매칭 "fips=yes" "provider=myprov" → 매칭되는 구현 선택 EVP API → 알고리즘 함수 호출 newctx → init → update → final → freectx CORE는 알고리즘 구현을 모름 OSSL_DISPATCH 테이블의 함수 포인터만 호출 커스텀 Provider (myprov.so) OSSL_provider_init() — 진입점 (필수) Provider 컨텍스트 (provctx) 생성 OSSL_DISPATCH[] — Provider 레벨 함수들 OSSL_ALGORITHM[] — 알고리즘 목록 Digest 구현 newctx() init() / update() final() / freectx() get_params() Cipher 구현 newctx() encrypt_init() update() / final() freectx() 각 알고리즘은 OSSL_DISPATCH[]로 함수 포인터 테이블을 제공 dlopen in out 호출
CORE는 Provider에게 유틸리티 함수(in)를 제공하고, Provider는 알고리즘 구현 함수 테이블(out)을 반환합니다. 양측은 OSSL_DISPATCH 배열로만 통신합니다.

Provider 연산 카테고리 (operation_id)

query_operation()에서 CORE가 요청하는 연산 ID별로 해당 알고리즘 목록을 반환합니다.

operation_id카테고리구현해야 할 함수들예시 알고리즘
OSSL_OP_DIGEST해시/다이제스트newctx, init, update, final, freectx, get_paramsSHA-256, SHA3-256
OSSL_OP_CIPHER대칭 암호newctx, encrypt_init, decrypt_init, update, final, freectx, get_params, set_ctx_paramsAES-256-GCM
OSSL_OP_MAC메시지 인증 코드newctx, init, update, final, freectxHMAC, CMAC
OSSL_OP_KDF키 파생 함수newctx, derive, freectx, set_ctx_paramsHKDF, PBKDF2
OSSL_OP_KEYMGMT키 관리new, gen_init, gen, free, has, import, export, get_paramsRSA, EC, Ed25519
OSSL_OP_KEYEXCH키 교환newctx, init, set_peer, derive, freectxECDH, X25519, DH
OSSL_OP_SIGNATURE디지털 서명newctx, sign_init, sign, verify_init, verify, freectxRSA-PSS, ECDSA
OSSL_OP_ASYM_CIPHER비대칭 암호화newctx, encrypt_init, encrypt, decrypt_init, decrypt, freectxRSA-OAEP
OSSL_OP_KEM키 캡슐화newctx, encapsulate, decapsulate, freectxRSA-KEM, PQC
OSSL_OP_RAND난수 생성newctx, instantiate, generate, freectxCTR-DRBG
OSSL_OP_STORE키/인증서 저장소open, load, eof, closefile:, pkcs11:
OSSL_OP_ENCODER직렬화 (출력)newctx, encode, freectxPEM, DER
OSSL_OP_DECODER역직렬화 (입력)newctx, decode, freectxPEM, 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;
}
실전 참고: 커스텀 Provider를 개발할 때는 OpenSSL 소스의 providers/implementations/ 디렉터리에 있는 Default Provider 구현을 참조하는 것이 가장 좋습니다. 특히 providers/implementations/digests/sha2_prov.cproviders/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 Google 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.xBoringSSL
버전 관리시맨틱 버전 (3.x.y)Git 커밋 해시만
Provider/ENGINEProvider 시스템없음 (단일 구현)
FIPSFIPS ProviderBoringCrypto 모듈
QUIC3.2+ 내장자체 QUIC API (선도)
EVP APIEVP_MD_fetch() 등 3.x APIEVP_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 이메일 주소 검증의 스택 오버플로 원격 코드 실행 가능 (제한적) 인증서 파싱 코드 감사 강화
커널 영향: Heartbleed는 사용자 공간 OpenSSL의 취약점이지만, 커널의 kTLS는 OpenSSL의 TLS 레코드 처리를 사용하지 않으므로 직접적인 영향을 받지 않았습니다. 그러나 커널 모듈 서명에 사용되는 OpenSSL의 X.509 파싱 코드(CVE-2022-3602)는 빌드 시스템 공급망 공격 벡터가 될 수 있어, 빌드 서버의 OpenSSL 버전 관리가 중요합니다.

취약점 방어 체크리스트

# 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 인프라를 구성합니다.

cert-manager 인증서 자동 발급/갱신 (Let's Encrypt, Vault) K8s Secret (TLS 인증서/키) Pod A (Service) Envoy Sidecar mTLS 종단 (BoringSSL/OpenSSL) 앱 컨테이너 평문 HTTP (TLS 불필요) localhost Volume: /etc/tls/{tls.crt, tls.key, ca.crt} Init Container: openssl verify -CAfile ca.crt tls.crt Pod B (Service) Envoy Sidecar mTLS 종단 (BoringSSL/OpenSSL) 앱 컨테이너 평문 HTTP Volume: /etc/tls/{tls.crt, tls.key, ca.crt} Init Container: openssl verify -CAfile ca.crt tls.crt mTLS (암호화된 서비스 간 통신)
Kubernetes에서 cert-manager가 인증서를 자동 발급하고, Envoy 사이드카가 mTLS를 투명하게 처리하여 앱 컨테이너는 TLS를 인식할 필요가 없습니다.

컨테이너 이미지의 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-managerKubernetes CRD인증서 검증 (Init Container)자동 (만료 30일 전)
Vault PKIHashiCorp VaultCSR 생성, 인증서 검증정책 기반 (TTL)
ACME (Let's Encrypt)certbot, Lego키 생성, CSR, 인증서 설치90일 (자동)
SPIFFE/SPIREWorkload IdentitySVID 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 API3.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과 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.

외부 참고 자료