728x90

 

OpenSSL Provider란?

기존 OpenSSL 1.1.x까지는 암호 알고리즘이 libcrypto 안에 하드코딩되어 있었습니다.
하지만 OpenSSL 3.0부터는 이런 암호 알고리즘 구현을 플러그인(Provider) 형태로 분리했습니다.

즉, Provider는 OpenSSL에서 사용할 수 있는 암호 알고리즘, 키 관리, 서명, 해시 등 암호 서비스들의 집합입니다.
OpenSSL 라이브러리 본체는 이 Provider들로부터 필요한 기능을 가져와서 실행합니다.

 

 왜 Provider를 도입했나?

  • 유연성
    새로운 알고리즘이나 하드웨어 가속 기능을 추가할 때 OpenSSL을 직접 수정하거나 재컴파일할 필요 없이, 별도 Provider만 추가하면 됩니다.
  • 정책 분리
    예: FIPS 인증된 알고리즘만 사용해야 할 경우, FIPS Provider만 로드하면 됩니다. 반대로 개발용에선 Default Provider를 쓰면 됩니다.
  • 사용자 정의
    개발자가 직접 자신만의 Provider를 작성해 OpenSSL에 플러그인처럼 붙일 수 있습니다.

 

OpenSSL 기본 제공 Provider 종류

OpenSSL 3.0 설치 시 기본으로 제공되는 Provider들은 다음과 같습니다:

Provider 이름 설명
default 대부분의 표준 알고리즘 (RSA, AES, SHA 등) 포함
fips FIPS 140-2 인증용 알고리즘 제공 (엄격한 보안 요구사항에 맞춤)
legacy 오래되었거나 약한 보안의 알고리즘 제공 (MD5, RC4 등)
null 어떤 암호 기능도 제공하지 않는 빈 Provider (주로 테스트용)

 

Provider의 동작 방식

OpenSSL은 실행 시 또는 설정 파일 (openssl.cnf)에서 로드할 Provider를 지정합니다.

  • 특정 알고리즘 요청 → OpenSSL은 등록된 Provider 목록을 탐색 → 해당 알고리즘을 제공하는 Provider에서 구현을 가져옴
  • 따라서 어떤 Provider를 로드하느냐에 따라 OpenSSL의 기능과 보안 수준이 달라질 수 있습니다.

 

간단한 예: Provider 사용

# FIPS Provider 로드 예제
export OPENSSL_MODULES=/usr/local/lib/ossl-modules
openssl list -providers

# 특정 Provider를 명시적으로 로드하는 코드 예제 (C)
OSSL_PROVIDER *provider = OSSL_PROVIDER_load(ctx, "fips");
if (provider == NULL) {
    fprintf(stderr, "FIPS provider load failed\n");
}

 

 

외부 Provider 예시

1. OpenSSL FIPS Provider (OpenSSL 공식)

  • OpenSSL 팀이 직접 제공하는 FIPS 140-2 인증용 Provider
  • 기존에는 별도 FIPS 모듈로 제공되었지만, 3.x부터는 Provider 형태로 통합됨.

2. Open Quantum Safe (OQS) Provider

  • 양자내성암호(Post-Quantum Cryptography) 알고리즘을 OpenSSL에서 쓸 수 있게 해주는 Provider
  • https://openquantumsafe.org
  • Dilithium, Kyber 등 NIST PQC 최종 후보 알고리즘을 OpenSSL에서 바로 실험할 수 있도록 지원.

3. SoftHSM + PKCS#11 Provider

  • 하드웨어 보안 모듈(HSM)이나 소프트웨어 HSM에서 제공하는 PKCS#11 인터페이스와 연결되는 Provider
  • 예: OpenSC 프로젝트, SoftHSM 등과 연동해 키 관리.

4. Intel QAT (QuickAssist) Provider

  • Intel QAT 하드웨어 가속 기능을 OpenSSL에서 사용하도록 만든 Provider
  • AES, RSA, ECDSA 등 연산을 QAT 장치로 오프로드.

5. NVIDIA GPU Provider (실험적)

  • NVIDIA GPU를 이용해 OpenSSL 암호 연산을 가속하는 Provider (주로 연구용, 아직 널리 사용되지는 않음).

 

 

커스텀 Provider 구현 방법

 

커스텀 Provider의 기본 구성

커스텀 Provider를 만들려면 크게 다음 요소가 필요합니다:

1. Provider 엔트리 포인트
→ OpenSSL이 이 모듈을 로드할 때 호출할 OSSL_provider_init() 함수를 구현해야 합니다.

int OSSL_provider_init(const OSSL_CORE_HANDLE *handle,
                       const OSSL_DISPATCH *in,
                       const OSSL_DISPATCH **out,
                       void **provctx);

 

이 함수는:

  • OpenSSL 코어 핸들 handle과
  • Provider가 코어에 제공할 함수 목록 out을 세팅하고
  • Provider 내부 상태를 담은 컨텍스트 provctx를 초기화합니다.

 

2. DISPATCH 테이블
→ Provider가 제공하는 기능들을 OSSL_DISPATCH 배열로 선언합니다.

예:

static const OSSL_DISPATCH my_provider_dispatch_table[] = {
    { OSSL_FUNC_PROVIDER_GETTABLE_PARAMS, (void (*)(void)) my_gettable_params },
    { OSSL_FUNC_PROVIDER_GET_PARAMS, (void (*)(void)) my_get_params },
    { OSSL_FUNC_PROVIDER_QUERY_OPERATION, (void (*)(void)) my_query_operation },
    { OSSL_FUNC_PROVIDER_TEARDOWN, (void (*)(void)) my_teardown },
    { 0, NULL }
};

 

 

3. 알고리즘 등록
Provider는 my_query_operation() 같은 함수에서 OpenSSL에게
“나는 이 연산(digest, cipher, signature, …)에 대해 이런 알고리즘들을 제공해”라고 알려줍니다.

예:

static const OSSL_ALGORITHM my_digests[] = {
    { "MYHASH", "provider=myprovider", myhash_functions },
    { NULL, NULL, NULL }
};

각 항목은:

  • 알고리즘 이름 (예: MYHASH)
  • 프로퍼티 스트링 (예: provider=myprovider)
  • 함수 테이블 (예: myhash_functions)
    로 구성됩니다.

 

4. 알고리즘 함수 구현
예를 들어 digest를 제공한다면:

  • newctx, freectx, init, update, final, get_params 같은 함수들을 정의해 OSSL_DISPATCH 테이블로 묶습니다.
static const OSSL_DISPATCH myhash_functions[] = {
    { OSSL_FUNC_DIGEST_NEWCTX, (void (*)(void)) myhash_newctx },
    { OSSL_FUNC_DIGEST_UPDATE, (void (*)(void)) myhash_update },
    { OSSL_FUNC_DIGEST_FINAL, (void (*)(void)) myhash_final },
    { OSSL_FUNC_DIGEST_FREECTX, (void (*)(void)) myhash_freectx },
    { 0, NULL }
};

 

 

 

개발 흐름 요약

1️⃣ .c 파일로 Provider 코드 작성
2️⃣ OSSL_provider_init()과 필요한 DISPATCH, ALGORITHM 테이블 정의
3️⃣ gcc -shared -fPIC -o myprovider.so myprovider.c -lcrypto로 공유 라이브러리 빌드
4️⃣ OpenSSL에서 환경 변수 또는 openssl.cnf로 모듈 경로 등록
5️⃣ OSSL_PROVIDER_load() 또는 openssl list -digest-algorithms 같은 명령으로 테스트

 

예제 구조

myprovider/
├── Makefile
├── myprovider.c      ← 메인 구현
├── myhash.c          ← 예: custom digest 알고리즘 구현
├── myprovider.so     ← 빌드 후 생성되는 동적 라이브러리

 주의할 점

  •  OpenSSL 내부 API는 잘 문서화되어 있지 않으므로, 기본 제공 Provider 소스 (예: default, legacy)를 참고하는 게 중요합니다.
  •  OpenSSL 버전 간 호환성 문제에 주의해야 합니다 (예: 구조체 변경, DISPATCH 상수 추가 등).
  •  Provider는 OpenSSL Core ↔ Module 형태로 통신하므로, 반드시 명세된 API 범위 내에서만 동작해야 합니다.
728x90
728x90

PKCS#7 패딩 개요

  • 블록 크기: 일반적으로 8바이트(64비트) 또는 16바이트(128비트)
  • 패딩 규칙:
    • 패딩이 필요한 경우: N 바이트를 패딩해야 한다면, N 바이트 모두 N으로 채움
    • 패딩이 필요 없는 경우(블록 크기의 정확한 배수): 전체 블록 크기만큼 패딩 추가 (block_size 바이트 모두 block_size 값으로 채움)

예시 (AES의 16바이트 블록 기준):

  • 원문: YELLOW SUBMARINE (16바이트) → 패딩 추가: 0x10 0x10 ... (16번)
  • 원문: YELLOW SUB (11바이트) → 패딩 추가: 0x05 0x05 0x05 0x05 0x05

 

PKCS#7 패딩 구현

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void pkcs7_pad(unsigned char *input, size_t input_len, size_t block_size,
               unsigned char **output, size_t *output_len) {
    size_t pad_len = block_size - (input_len % block_size);
    *output_len = input_len + pad_len;
    *output = (unsigned char *)malloc(*output_len);
    if (*output == NULL) {
        perror("malloc failed");
        exit(1);
    }

    memcpy(*output, input, input_len);
    memset(*output + input_len, (unsigned char)pad_len, pad_len);
}

int pkcs7_unpad(unsigned char *input, size_t input_len,
                unsigned char **output, size_t *output_len) {
    if (input_len == 0) return -1;

    unsigned char pad_len = input[input_len - 1];

    if (pad_len == 0 || pad_len > input_len) return -1;

    for (size_t i = 0; i < pad_len; ++i) {
        if (input[input_len - 1 - i] != pad_len)
            return -1;  // Invalid padding
    }

    *output_len = input_len - pad_len;
    *output = (unsigned char *)malloc(*output_len);
    if (*output == NULL) {
        perror("malloc failed");
        return -1;
    }

    memcpy(*output, input, *output_len);
    return 0;
}

 

 

 

728x90
728x90

Windows: DLL에서 함수/심볼 Export하기

Windows의 DLL에서는 내보낼 함수에 대해 명시적으로 export 지시를 해주어야 합니다. 기본적으로 DLL에 정의된 함수들은 외부에 자동 공개되지 않으며, 외부에 제공하려는 함수들은 export 대상임을 표시해야 합니다. 이를 위해 Microsoft 컴파일러에서는 __declspec(dllexport) 키워드를 사용합니다. 반대로, DLL에 들어있는 함수를 외부 애플리케이션에서 사용할 때는 __declspec(dllimport)로 가져오는(import) 함수임을 표시합니다. 예를 들어, DLL을 만들 때 헤더에서 함수를 다음과 같이 선언합니다:

// mylib.h (DLL을 빌드할 때)
__declspec(dllexport) int add(int a, int b);

위와 같이 선언하고 DLL을 빌드하면 add 함수가 DLL의 외부 Export 심볼 테이블에 등록되어 다른 프로그램에서 접근할 수 있게 됩니다. DLL을 사용하는 애플리케이션 쪽에서는 해당 함수를 선언할 때 import 지시어를 붙여주는 것이 일반적입니다:

// 사용 측 애플리케이션 코드
__declspec(dllimport) int add(int a, int b);  // DLL에서 가져올 함수 선언
...
int result = add(2, 3);  // DLL의 함수를 호출

dllimport로 표시하면 컴파일러가 DLL의 외부 함수임을 인지하여 적절한 호출 코드를 생성합니다. 참고로, 함수 사용 측에서 dllimport를 생략해도 동작은 가능하지만, 명시하는 것이 올바른 사용법이며 컴파일러 최적화에 유리합니다.

헤더 파일 구성: DLL을 만들 때와 사용할 때 서로 다른 선언(dllexport vs dllimport)이 필요하지만, 이를 위해 헤더 파일을 두 벌로 나누지 않고 보통 매크로를 이용해 하나의 헤더로 관리합니다. 예를 들어 DLL을 빌드할 때 특정 매크로(MYLIB_EXPORTS 등)를 정의해 두면, 헤더에서 해당 매크로가 정의된 경우 dllexport를, 아닌 경우 dllimport를 붙이도록 구현합니다. 코드 예시는 다음과 같습니다:

// mylib_export.h - Windows DLL용 매크로 정의
#ifdef MYLIB_EXPORTS        // DLL 빌드 시에는 이 매크로를 정의
  #define MYLIB_API __declspec(dllexport)
#else                       // DLL을 사용하는 경우
  #define MYLIB_API __declspec(dllimport)
#endif

// mylib.h - 함수 선언부
#include "mylib_export.h"
MYLIB_API int add(int a, int b);

위와 같이 하면 DLL을 빌드할 때는 MYLIB_EXPORTS를 정의하여 컴파일하고, DLL 사용자 측에서는 해당 매크로를 정의하지 않으면 자동으로 dllimport가 적용된 선언을 사용하게 됩니다. 이러한 방식으로 하나의 헤더 파일로 DLL 내보내기/가져오기를 관리할 수 있습니다.

 

 

컴파일 및 링크 옵션 (Windows):

  • 컴파일 시 정의: DLL 빌드 시 MYLIB_EXPORTS와 같은 매크로를 정의(/D MYLIB_EXPORTS 또는 -DMYLIB_EXPORTS)해야 합니다. Visual C++의 DLL 프로젝트를 생성하면 보통 프로젝트 이름에 _EXPORTS 매크로가 자동 정의되어 위와 같은 조건을 만족하도록 해줍니다.
  • DLL 생성 플래그: MSVC에서는 /LD 옵션을 사용하여 DLL을 생성합니다. 예를 들어 명령행에서 컴파일하면:위 명령은 mylib.dll DLL을 생성하고, 함께 mylib.lib 라는 import 라이브러리도 생성합니다. .lib import 라이브러리는 DLL의 함수들을 링커가 연결할 수 있도록 하는 정보가 담긴 파일로, 응용 프로그램을 빌드할 때 필요합니다. 런타임에는 .dll 파일만 있으면 되지만, 컴파일 단계에서 링커는 .lib를 통해 외부 함수 참조를 해결합니다
  • cl /LD /D MYLIB_EXPORTS mylib.c /Fe:mylib.dll
  • MinGW(gcc) 사용 시: GCC 컴파일러를 사용해 Windows DLL을 만드는 경우에도 소스 코드에서는 동일하게 __declspec(dllexport)를 사용하며, -shared 옵션으로 DLL을 생성합니다. 예를 들어:(GCC로 DLL을 만들 때도 dlltool 등을 사용하여 import 라이브러리(.a)를 생성할 수 있습니다. 하지만 보통 MSVC 환경에서 .lib를 이용해 링크하거나, 필요 시 GCC의 -Wl,--out-implib 옵션으로 import 라이브러리를 얻을 수도 있습니다.)
  • gcc -shared -o mylib.dll mylib.c -DMYLIB_EXPORTS

Linux: 공유 객체(.so)에서 함수/심볼 Export하기

Linux의 공유 라이브러리(.so)는 Windows와 달리 별도의 export 지시어 없이도 전역 심볼은 기본적으로 외부에 공개됩니다. GCC를 사용해 -shared 옵션으로 컴파일하면, static이 아닌 전역 함수들은 자동으로 .so의 심볼 테이블에 노출되어 다른 프로그램이 사용할 수 있습니다. 예를 들어 간단한 C 소스를 공유 라이브러리로 빌드하면, 특별한 지시어 없이도 그 함수들은 외부에서 extern 선언으로 사용 가능합니다.

gcc -fPIC -shared -o libmylib.so mylib.c    # libmylib.so 생성 (기본적으로 모든 전역함수 export)

 

위 명령처럼 -fPIC (Position Independent Code) 옵션을 주어 오브젝트 파일을 생성하고, -shared 옵션으로 .so 파일을 링크합니다. 이렇게 하면 mylib.c에 정의된 전역 함수들이 libmylib.so의 공개 API로서 노출됩니다.

심볼 가시성 제어 (선택 사항): 기본 동작은 모든 전역 심볼 공개이지만, 원하지 않는 내부 함수까지 노출되지 않도록 제어하는 것이 권장됩니다. GCC에서는 -fvisibility 옵션으로 기본 심볼 가시성을 조절할 수 있습니다. 기본값 -fvisibility=default에서는 모든 심볼이 공개되지만, 이를 **-fvisibility=hidden**으로 변경하면 기본적으로 모든 심볼을 숨기고, 특정 함수에 한해 __attribute__((visibility("default"))) 속성을 부여하여 선택적으로 공개할 수 있습니다.

즉, 라이브러리를 컴파일할 때 -fvisibility=hidden 플래그를 주고, 헤더 파일에서 외부 API로 공개할 함수 선언에만 아래와 같은 속성(attribute)을 붙입니다:

// mylib.h (Linux에서 가시성 제어 예시)
#ifdef __GNUC__
  #define MYLIB_API __attribute__((visibility("default")))
#else
  #define MYLIB_API
#endif

MYLIB_API int add(int a, int b);   // 외부에 공개할 함수

 

이렇게 하면 add 함수만 공유 라이브러리의 외부 심볼로 노출되고, 그 외의 전역 함수들은 숨겨지게 됩니다. 이 기법을 사용하면 필요한 API만 노출할 수 있을 뿐 아니라, 바이너리 크기 감소, 링크 충돌 회피, 로드 시간 단축 등의 이점이 있습니다. 숨겨진 심볼은 외부에서 참조할 수 없으므로, 다른 라이브러리와의 이름 충돌을 방지하고 불필요한 심볼로 인한 오버헤드를 줄일 수 있습니다

헤더 파일 매크로 처리: 크로스 플랫폼을 고려하여, Windows와 Linux에서 모두 사용할 수 있는 매크로를 정의할 수 있습니다. 예를 들어 하나의 MYLIB_API 매크로를 만들어 Windows에서는 __declspec(dllexport/dllimport)을, Linux에서는 __attribute__((visibility("default")))로 매핑하면 동일한 헤더 코드로 양쪽을 처리할 수 있습니다. Windows에서는 빌드 시 export 매크로를 활성화하고, Linux에서는 컴파일러 특성만 부여하는 형태입니다.

크로스플랫폼 예제 코드 및 빌드

아래에는 Windows와 Linux에서 모두 사용 가능한 형태의 공유 라이브러리 예제를 제공합니다. 간단한 add 함수를 라이브러리 API로 공개하는 코드이며, 매크로로 플랫폼별 export 규칙을 처리합니다:

/* mylib.h - 공용 헤더 (Windows & Linux) */
#if defined(_WIN32) || defined(_WIN64)
  #ifdef MYLIB_EXPORTS
    #define MYLIB_API __declspec(dllexport)
  #else
    #define MYLIB_API __declspec(dllimport)
  #endif
#elif defined(__GNUC__) && __GNUC__ >= 4
  #define MYLIB_API __attribute__((visibility("default")))
#else
  #define MYLIB_API
#endif

// 외부에 공개할 함수 선언
MYLIB_API int add(int a, int b);

#ifdef __cplusplus
extern "C" {
#endif

// ... (다른 공개 함수들 선언 가능)

#ifdef __cplusplus
}
#endif

/* mylib.c - 구현 파일 */
#include "mylib.h"

MYLIB_API int add(int a, int b) {
    return a + b;
}

// (내부 전용 함수는 static으로 정의하거나, Linux의 경우 visibility("hidden") 등을 활용)
  • MYLIB_EXPORTS 매크로는 라이브러리를 빌드할 때 정의합니다. 이렇게 하면 Windows에서는 MYLIB_API가 __declspec(dllexport)로 설정되어 함수 add가 DLL의 export 심볼로 추가되고, Linux에서는 MYLIB_API가 visibility("default")로 설정되어 (혹은 -fvisibility=hidden 빌드 시) 해당 함수가 공개 심볼이 됩니다. 라이브러리를 사용하는 코드에서는 MYLIB_EXPORTS를 정의하지 않고 헤더를 포함하므로 Windows의 경우 dllimport로 선언되며, Linux의 경우 일반 선언과 다르지 않게 동작합니다.

빌드 방법:

  • Windows (MSVC): 위 코드로 DLL을 만들려면, 컴파일 단계에서 MYLIB_EXPORTS를 정의하고 /LD 옵션으로 DLL을 생성합니다. 예를 들어 명령어:이 명령을 실행하면 mylib.dll과 import 라이브러리 mylib.lib가 생성됩니다. 생성된 DLL을 다른 프로젝트에서 사용할 때는 mylib.h를 포함하고 mylib.lib를 링크하면, add 함수를 호출할 수 있습니다.
  • cl /LD /D MYLIB_EXPORTS mylib.c /Fe:mylib.dll
  • Linux (GCC): 공유 객체(.so)를 만들 때는 포지션 독립 코드를 위해 -fPIC 옵션을 주고, 공유 라이브러리로 빌드하기 위해 -shared 옵션을 사용합니다. 예를 들어:이렇게 하면 libmylib.so가 생성되며, 기본 설정에서는 add 함수 등 전역 함수들이 모두 공개 심볼로 포함됩니다. 만약 내부 함수는 숨기고 add만 export하려면 컴파일시에 -fvisibility=hidden 플래그를 추가하고, 위 헤더에서 정의한 대로 MYLIB_API 속성을 붙인 함수만 공개됩니다. 생성된 libmylib.so는 표준 라이브러리 디렉토리에 두거나, 컴파일 시에 -L 옵션으로 경로를 지정하고 -lmylib로 링크하여 사용할 수 있습니다.
  • gcc -fPIC -shared -o libmylib.so mylib.c
728x90

'Programming > C, C++' 카테고리의 다른 글

OpenSSL Provider에 대한 개념  (0) 2025.05.03
블록암호 PKCS#7 패딩 기능 C언어 구현  (0) 2025.04.20

+ Recent posts