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

Apple의 코드사인(Code Signing) 메커니즘은 macOS, iOS 등 Apple 플랫폼에서 실행 파일(앱, 프레임워크, dylib 등)의 무결성, 인증, 신뢰성을 보장하기 위한 보안 기술입니다. 

 

 Apple 코드사인 메커니즘 개요

1. 목적

  • 코드가 수정되지 않았음 (무결성 확인)
  • 코드의 출처가 신뢰할 수 있음 (인증)
  • 시스템이 해당 코드를 허용해도 되는지 판단 (신뢰성 검증)

코드사인의 구성요소

Apple의 코드서명은 Mach-O 파일 내부와 함께 .app 번들 외부에도 정보가 포함됩니다.

1. Code Signature (서명 블록)

Mach-O 파일 끝에 추가되는 구조체이며, 아래 정보 포함:

  • CodeDirectory: 해시 테이블 및 서명 메타데이터
  • CMS Signature: 실제 서명 (X.509 인증서 기반 CMS 구조)
  • Entitlements: 앱 권한 (예: 앱이 카메라 접근 가능한지 등)
  • Requirements: 실행 정책 요구사항 (예: Team ID 매칭)

2. X.509 인증서 체인

  • 인증서에는 Apple Developer ID 또는 Apple 공인 서명자가 포함되어야 함.
  • 이 인증서는 codesign 명령어로 서명할 때 같이 포함됨.

3. Mach-O 내 서명 데이터 위치

  • __LINKEDIT 세그먼트의 마지막에 서명 정보가 추가됨.
  • otool로 보면:
  • LC_CODE_SIGNATURE dataoff = <offset> datasize = <size>

 

 코드사인의 동작 원리

1. 앱/라이브러리 서명

개발자는 다음 명령어로 Mach-O 파일을 서명합니다:

codesign -s "Developer ID Application: Your Name (TEAMID)" binary_or_app

이 과정에서:

  • 바이너리의 각 섹션을 해시 → CodeDirectory 생성
  • CodeDirectory에 대해 CMS 서명 수행
  • 전체 서명 구조를 Mach-O 파일의 끝에 삽입

2. 앱 실행 시 검증 흐름

시스템 또는 커널에서 다음을 확인:

  1. 서명이 존재하는지 (LC_CODE_SIGNATURE 확인)
  2. CodeDirectory의 해시가 CMS 서명에 포함된 인증서로 검증되는지
  3. 실행 중인 Mach-O 바이너리의 섹션들이 CodeDirectory에 정의된 해시와 일치하는지
  4. Entitlements, Requirements 등 정책 조건 충족 여부 확인

3. System Integrity Protection (SIP), Gatekeeper와 연계

  • macOS의 Gatekeeper는 App Store 외부에서 실행되는 앱의 서명을 강제 확인
  • SIP는 시스템 영역의 바이너리 수정 자체를 방지

 코드 무결성 검증 방식

  • Mach-O의 특정 영역만 서명에 포함됩니다:
    • 일반적으로 __TEXT, __DATA 등의 실제 코드/데이터 영역
    • 서명 자체가 들어간 __LINKEDIT는 제외됨
  • 따라서 서명 이후 __TEXT가 바뀌면 무조건 서명 검증 실패

 

애플 코드사인이 깨지는 조건

원인 설명
바이너리 섹션 수정 __TEXT, __DATA 등 서명된 코드 변경
데이터 추가 Mach-O 파일 끝에 HMAC 등 추가
서명된 리소스 변경 Info.plist, entitlements 등 수정
인증서 문제 만료/철회된 개발자 인증서
번들 구조 변경 .app 내부 파일 추가/삭제/이동
dylib 로딩 오류 dylib 위치, 서명, 무결성 문제

 

728x90

'Programming > IOS, macOS, Android' 카테고리의 다른 글

apple IOS macOS의 Mach-O 파일 구조 개념  (0) 2025.04.24
728x90

Mach-O(Mach Object) 파일 포맷은 macOS, iOS 등 Apple 운영체제에서 사용되는 실행 파일, 라이브러리(.dylib), 커널 모듈 등 바이너리의 파일 포맷입니다. ELF(Linux)나 PE(Windows)와 같은 목적을 가지며, 모듈화된 구조와 다양한 아키텍처 지원을 위해 설계되었습니다.

Mach-O 파일의 기본 구조

+----------------------+
| Mach Header          | ← 파일 시작
+----------------------+
| Load Commands        |
+----------------------+
| Segment & Section    | ← 실제 바이너리 코드 및 데이터
|   (__TEXT, __DATA 등)|
+----------------------+
| Symbol Table (선택) |
+----------------------+
| String Table (선택) |
+----------------------+

 

1.  Mach Header

파일의 기본 정보를 담고 있습니다. 예:

  • CPU 아키텍처 (x86_64, arm64 등)
  • 파일 타입 (실행파일, dylib, bundle 등)
  • load command 개수
  • 32bit / 64bit 여부
struct mach_header_64 {
    uint32_t magic;          // 0xFEEDFACF (64비트), 0xFEEDFACE (32비트)
    cpu_type_t cputype;
    cpu_subtype_t cpusubtype;
    uint32_t filetype;
    uint32_t ncmds;
    uint32_t sizeofcmds;
    uint32_t flags;
    uint32_t reserved;       // only for 64-bit
};

 

 

2. Load Commands

Mach-O의 중앙 디렉터리 같은 개념입니다. 파일 안에 있는 세그먼트 정보, 심볼, 동적 라이브러리, Entry point 등 메타데이터를 정의합니다.

Load Command는 LC_*로 시작하는 다양한 타입이 있습니다.

예시:

  • LC_SEGMENT_64: 세그먼트를 정의함
  • LC_SYMTAB: 심볼 테이블 정보
  • LC_LOAD_DYLIB: 외부 .dylib 로딩 정보
  • LC_MAIN: main entry point (iOS/macOS에서)

 

3.  Segment & Section

세그먼트(Segment)는 메모리에 매핑되는 논리적 블록이고, 섹션(Section)은 세그먼트 내에 존재하는 세부 블록입니다.

주요 세그먼트

세그먼트 설명
__TEXT 실행 코드, 상수, 문자열
__DATA 초기화된 전역 변수, HMAC 저장 가능
__LINKEDIT 심볼, 디버깅 정보 등
__PAGEZERO NULL 포인터 방지용 (읽기/쓰기 불가 메모리)

 

주요 섹션

각 세그먼트는 여러 섹션을 포함합니다.

세그먼트섹션 이름설명

세그먼트 섹션 설명
__TEXT __text 실제 코드
__TEXT __cstring 문자열 상수
__DATA __data 초기화된 데이터
__DATA __bss 초기화되지 않은 데이터 (런타임에 0으로 초기화됨)

 

4.  Symbol Table (옵션)

심볼 테이블은 함수명, 전역변수명 등 심볼의 이름과 주소를 연결합니다. nm, objdump 같은 도구로 분석 가능.


5. String Table (옵션)

심볼의 이름 등 문자열 정보가 여기에 저장됩니다.

 

mach-o 파일 구조 분석 도구 otool

otool은 macOS에서 Mach-O 파일을 분석하는 명령어입니다.
리눅스의 objdump, readelf, nm과 유사하게, 바이너리의 구조, 섹션, 심볼, 라이브러리 종속성 등을 확인할 수 있습니다.

$otool -l mylib.dylib

 

출력 예시:

Load command 0
      cmd LC_SEGMENT_64
  cmdsize 712
  segname __TEXT
   vmaddr 0x0000000100000000
   ...
   Section
     sectname __text
     segname  __TEXT
     addr     0x100000f60
     size     0x0000001c0

 

 

옵션 설명
-h Mach-O 헤더 출력
-l Load Commands 출력
-L 연결된 외부 .dylib 목록 (LC_LOAD_DYLIB)
-t __TEXT, __text 섹션의 기계어 출력 (디스어셈블링 없이)
-v 기계어를 가독성 있게 출력
-V 기계어를 AT&T 어셈블리로 디스어셈블
-s <segment> <section> 특정 세그먼트/섹션의 헥사 값 출력
-S 모든 섹션의 내용을 출력
-r 재배치 정보 (relocation entries) 출력
-M Objective-C 메서드 목록 출력
-I indirect symbol table 출력
-dyld_info dyld 정보 출력 (lazy symbol, bindings 등)

 

 

728x90

'Programming > IOS, macOS, Android' 카테고리의 다른 글

Apple 코드사인(CodeSign) 개념  (0) 2025.04.24
728x90

Java 암호화 아키텍처(JCA) 개요

 

JCA의 개념 및 구조

 

Java Cryptography Architecture(JCA)는 자바 플랫폼에서 암호화 관련 기능을 제공하는 핵심 프레임워크로, 각종 보안 서비스를 통합된 방식으로 지원합니다. 예를 들어 JCA는 전자서명(Digital Signature), 메시지 다이제스트(Message Digest, 해시), 인증서 및 인증서 유효성 검사, 암호화(대칭/비대칭), 키 생성 및 관리, 보안 난수 생성 등 현대 암호기술에 필요한 거의 모든 기능을 포함합니다 . JCA는 “프로바이더(provider) 아키텍처”라는 플러그인 구조를 기반으로 설계되어, 구현으로부터 독립적인 표준 API와 여러 알고리즘의 유연한 탑재를 가능하게 합니다. 이러한 설계로 구현 독립성, 구현 상호운용성, 알고리즘 확장성 같은 원칙을 충족하며 , 개발자는 보안 알고리즘의 이론적 세부 내용을 직접 구현하지 않고도 표준 API 호출만으로 필요한 보안 서비스를 사용할 수 있습니다 .

 

JCA는 내부적으로 프레임워크와 프로바이더 구현의 두 부분으로 구성됩니다 . 즉, 표준화된 보안 API 집합(예: java.security, javax.crypto 등의 패키지로 제공되는 클래스들)과, 다양한 알고리즘의 실제 구현체들을 담은 암호 서비스 프로바이더(Cryptographic Service Provider)들이 존재합니다. 자바 플랫폼에는 기본적으로 Sun, SunJCE, SunRsaSign 등의 여러 프로바이더 구현체가 내장되어 있으며 , JCA는 이들 중 우선순위가 가장 높은 프로바이더로부터 요청된 알고리즘의 구현체를 선택하여 제공합니다. 또한 JCA는 서드파티 프로바이더의 추가를 지원하여, 기본 제공 알고리즘 외에도 새로운 알고리즘 구현을 손쉽게 확장할 수 있습니다 . (JDK 1.4부터는 원래 별도였던 JCE(Java Cryptography Extension)도 JCA에 통합되어, 대칭키 암호화 등의 기능 역시 모두 동일한 구조로 제공됩니다.)

 

 

프로바이더 아키텍처와 엔진 클래스

 

JCA의 프로바이더 아키텍처에서는 엔진 클래스(engine class)와 프로바이더(provider)가 상호 작용하여 암호 서비스를 제공합니다. 엔진 클래스란 암호화 서비스의 동작을 표현하는 고수준 표준 API 클래스로, 특정 알고리즘이나 구현에 독립적인 추상 인터페이스를 담당합니다 . 예를 들어 MessageDigest, Signature, Cipher 등은 각각 해시, 서명, 암호화 등의 기능을 수행하는 엔진 클래스들입니다. 한편 프로바이더는 이러한 엔진 클래스가 사용할 알고리즘의 실제 구현체를 제공하는 플러그인 모듈입니다. 모든 프로바이더는 java.security.Provider 클래스를 상속하여 구현되며, 자신이 제공하는 알고리즘 및 구현체 목록을 내부에 유지합니다 . 프로바이더는 JRE의 설정 파일(java.security)이나 애플리케이션 코드에서 등록할 수 있고, JCA는 등록된 프로바이더들을 순서에 따라 관리합니다 . 여러 프로바이더가 동일한 알고리즘을 구현하는 경우, 우선순위(preference order)에 따라 가장 높은 우선순위 프로바이더의 구현체가 사용되며, 필요하면 애플리케이션이 특정 프로바이더를 지명할 수도 있습니다 .

 

JCA에서 암호 서비스를 사용하는 방법은 일관된 패턴을 따릅니다. 애플리케이션은 엔진 클래스의 정적 팩토리 메서드 getInstance()에 알고리즘 이름(및 필요시 프로바이더)을 전달하여 해당 서비스 객체를 얻습니다 . 예를 들어 SHA-256 해시 계산을 위해 MessageDigest.getInstance("SHA-256")을 호출하면, JCA는 설치된 프로바이더 중 SHA-256 알고리즘을 제공하는 첫 번째 구현체를 찾아서 MessageDigest 객체로 반환합니다 . 특정 프로바이더의 구현을 쓰고 싶다면 MessageDigest.getInstance("SHA-256", "Provider명")처럼 프로바이더를 지정할 수도 있습니다 . 이렇게 얻어진 엔진 클래스 인스턴스(MessageDigest 등)는 내부적으로 해당 프로바이더의 실제 구현 객체를 캡슐화하고 있으며, 엔진 클래스의 메서드를 호출하면 내부적으로 연결된 구현체(SPI)의 메서드를 호출하여 실제 동작을 수행합니다 . JCA에서 각 엔진 클래스마다 그에 대응하는 서비스 제공자 인터페이스(SPI) 추상 클래스(e.g., SignatureSpi, CipherSpi)가 정의되어 있으며, 프로바이더는 이 SPI를 상속하여 알고리즘 구현을 제공하게 됩니다 . 엔진 클래스는 이러한 SPI 구현 객체를 자신의 필드로 품고 있으며, 최종 사용자에게는 일관된 API를 제공하면서 내부적으로는 다양한 구현체와 상호 작용하는 것입니다.

 

이러한 구조 덕분에 애플리케이션 개발자는 구현체에 의존하지 않는 코드를 작성할 수 있습니다. 예를 들어 동일한 Cipher API를 사용하더라도, 기본 JDK 프로바이더의 AES 구현이나 서드파티 BouncyCastle 프로바이더의 AES 구현을 교체하여 사용할 수 있습니다. 보통은 JDK에 내장된 프로바이더들이 폭넓은 알고리즘을 충분히 제공하므로 특별히 프로바이더를 지정하지 않고 사용하는 경우가 많습니다 . (JCA는 설치된 프로바이더와 그 제공 서비스들을 조회하거나 관리하는 API도 제공하며, Security 클래스 등을 통해 현재 등록된 프로바이더 목록이나 알고리즘 목록을 얻을 수 있습니다 .)

 

 

JCA가 제공하는 보안 서비스

 

JCA는 애플리케이션 보안을 위해 다양한 암호화 서비스 API를 제공합니다 . 주요 서비스 범주는 다음과 같습니다 :

 

  • 암호화/복호화: 대칭 키 알고리즘(AES, DES 등)과 비대칭 키 알고리즘(RSA, ECC 등)을 이용하여 데이터를 암호화하고 복호화하는 기능입니다. 이 기능은 Cipher 클래스를 통해 제공되며, 블록 암호, 스트림 암호, 패스워드 기반 암호화(PBE) 등 여러 종류의 알고리즘 모드를 지원합니다 .
  • 디지털 서명: 전자 서명을 생성하고 검증하는 기능으로, Signature 클래스를 통해 제공됩니다. 개인 키로 데이터를 서명하고 대응되는 공개 키로 서명을 검증함으로써, 데이터의 무결성 송신자 인증을 보장합니다 . (예: RSA 또는 DSA 서명 알고리즘을 이용한 전자서명.)
  • 메시지 다이제스트(해시): 임의 길이의 입력 데이터를 고정 길이의 해시 값으로 변환하는 기능입니다. MessageDigest 클래스를 통해 SHA-1, SHA-256, SHA-512 등의 해시 알고리즘을 사용할 수 있으며, 생성된 해시는 데이터 변경 여부를 확인하기 위한 무결성 체크 등에 활용됩니다 .
  • 메시지 인증 코드(MAC): 공유된 비밀 키를 사용하여 메시지로부터 MAC 값을 생성하고 검증하는 기능입니다. Mac 클래스를 통해 제공되며, 해시 함수와 비밀 키를 조합하여 생성된 MAC 값으로 데이터의 무결성과 인증을 보장합니다 . (주로 송신자와 수신자가 동일한 비밀 키를 공유하는 환경에서 사용)
  • 키 생성 및 교환: 암호화에 사용되는 **키(key)**를 생성하거나 여러 당사자가 키를 안전하게 공유하는 기능입니다. 예를 들어 대칭키를 생성하는 KeyGenerator 클래스, 공개/개인키 쌍을 생성하는 KeyPairGenerator클래스, 그리고 키 합의(Key Agreement)를 수행하는 KeyAgreement 클래스 등이 포함됩니다. KeyAgreement을 통해 Diffie-Hellman 같은 프로토콜을 구현하여 둘 이상의 당사자가 공동의 비밀 키를 합의할 수 있습니다 .
  • 키 저장 및 관리: 생성된 키와 인증서를 안전하게 보관하고 관리하는 기능입니다. JCA는 키스토어(KeyStore) 라는 키/인증서 저장소 개념을 제공하며, KeyStore 클래스를 통해 파일 등의 형태로 키를 저장/로드할 수 있습니다. 키스토어에는 개인 키와 해당 인증서 체인, 신뢰된 인증서 등이 저장되어 신원 인증  키 관리에 활용됩니다 . 또한 X.509 인증서의 생성과 파싱은 CertificateFactory 클래스로 처리할 수 있으며, 인증서 경로 검증(CertPathValidator)이나 인증서 철회 목록(CRL) 처리 등의 PKI 관련 기능도 지원됩니다 (Java의 표준 PKI API를 통해 제공) .

 

이러한 서비스들을 구현하기 위해 JCA에는 다양한 API 클래스들이 제공되며, 암호 키 Key 인터페이스를 통해 일관된 형태로 취급됩니다. 공개키/개인키/대칭키 등의 모든 Key 객체는 Key 인터페이스를 상속한 형태로 표현되고, 키의 알고리즘 이름, 인코딩 형식, 바이너리 표현을 얻는 메서드를 공통적으로 제공합니다 . 애플리케이션은 엔진 클래스 (Cipher, Signature 등)의 인스턴스를 초기화할 때 이러한 Key 객체를 전달하며, 엔진 클래스는 전달된 키를 이용해 암호 연산을 수행합니다 . 아래에서는 JCA에서 주로 사용되는 주요 클래스와 인터페이스를 정리합니다.

 

 

주요 API 클래스 및 인터페이스 역할 정리

클래스/인터페이스 역할 및 용도
Provider JCA에 플러그인 방식으로 등록되는 암호 서비스 제공자를 표현하는 클래스입니다. 제공 가능한 알고리즘들과 해당 구현체들의 **목록(데이터베이스)**을 보유하여, 요청 시 JCA 프레임워크가 적절한 구현 클래스를 찾을 수 있도록 합니다 . 즉, 알고리즘 이름을 키(Key)로 하고 구현 클래스 정보를 값(Value)으로 가지는 맵 구조로 알고리즘들을 광고하며, 표준 API 호출이 실제 구현체와 연결되도록 해 줍니다.
Security  JCA의 보안 설정과 프로바이더 관리를 담당하는 유틸리티 클래스입니다. 보안 프로퍼티와 프로바이더 목록을 중앙에서 관리하며, 프로바이더 등록/제거 (Security.addProvider(...) 등)이나 설치된 프로바이더 열람 (Security.getProviders() 등)과 같은 정적 메서드를 제공합니다 . 애플리케이션은 이 클래스를 통해 현재 사용 가능한 프로바이더와 알고리즘 정보를 조회하거나 동적으로 새로운 프로바이더를 추가할 수 있습니다.
SecureRandom 암호학적 난수 생성기를 제공하는 클래스입니다. 예측 불가능한 난수를 생성하기 위해 내부적으로 시드(seed)를 활용한 의사난수 알고리즘을 사용하며, 키 생성이나 난수 challeng 등 보안상 중요한 용도의 난수 값을 공급합니다 . JDK 기본 구현은 OS의 엔트로피 소스를 활용하여 높은 품질의 난수를 제공합니다.
MessageDigest 입력 데이터의 **메시지 다이제스트(해시)**를 계산하는 엔진 클래스입니다. MD5, SHA-256 등 해시 알고리즘을 선택하여 getInstance로 객체를 생성한 뒤, update() 메서드로 데이터를 입력하고 digest()를 호출하면 해시 값을 얻을 수 있습니다 . 해시 함수는 입력 데이터의 무결성 검증이나 비밀번호 저장 등의 목적으로 사용됩니다.
Signature 전자서명 생성 및 검증을 위한 엔진 클래스입니다. initSign(개인키)으로 서명용으로 초기화하여 update()로 데이터를 입력한 뒤 sign()을 호출하면 서명값을 생성하고, initVerify(공개키)  update() verify(서명값)를 호출하면 서명을 검증합니다. 내부적으로 RSA, DSA, ECDSA 등 다양한 공개키 서명 알고리즘을 지원하며, 키 쌍을 통해 데이터의 인증과 무결성을 보장합니다 .
Cipher 데이터 암호화/복호화를 위한 엔진 클래스입니다. init(모드, 키) 메서드로 암호화(ENCRYPT_MODE) 또는 복호화(DECRYPT_MODE) 모드 및 사용할 키를 지정하여 초기화하고, doFinal(데이터) 등을 호출하면 데이터를 암호화하거나 복호화합니다. AES, DES 같은 대칭키 블록 암호, RC4 같은 스트림 암호, RSA같은 비대칭키 암호, 그리고 PBE(Password-Based Encryption) 등 다양한 알고리즘을 지원하며, 알고리즘/모드/패딩 조합을 문자열로 지정하여 세부 구성을 선택할 수 있습니다 .
Mac 메시지 인증 코드(Message Authentication Code)를 생성/검증하는 엔진 클래스입니다. init(키)로 비밀 키를 설정한 후 update(데이터) doFinal()을 호출하여 MAC 값을 생성합니다. MAC은 해시 함수와 비밀 키를 조합하여 생성한 값으로, 송수신자가 공유한 키로만 검증될 수 있으므로 메시지의 무결성 인증을 보장합니다 . (예: HMAC-SHA256 등 해시 기반 MAC 알고리즘)
KeyPairGenerator 공개키 암호에서 사용하는 **키 쌍(공개키 + 개인키)**을 생성하는 클래스입니다. RSA, EC 등의 알고리즘 이름을 지정하여 getInstance로 객체를 얻고 키 크기 등을 설정한 후 generateKeyPair()를 호출하면 새로운 키쌍(KeyPair 객체)을 생성합니다 . 주로 공개키 기반의 인증서 발급이나 서명 키 생성 등에 사용됩니다.
KeyGenerator 대칭키 암호에서 사용하는 **비밀 키(대칭 키)**를 생성하는 클래스입니다. 예를 들어 AES 알고리즘용 키를 만들기 위해 KeyGenerator.getInstance("AES")로 객체를 얻고 키 크기를 설정한 뒤 generateKey()를 호출하면 SecretKey 객체를 생성합니다 . 생성된 비밀 키는 Cipher 등에 전달되어 암호화에 활용됩니다.
KeyFactory /
SecretKeyFactory
**키 변환(Key Conversion)**을 위한 클래스입니다. 외부에서 받은 키 자료(예: X.509로 인코딩된 공개키 바이트 배열 등)를 자바의 Key 객체로 변환하거나, 반대로 Key 객체를 키 명세(KeySpec) 형태의 데이터로 변환할 때 사용합니다 . KeyFactory는 공개키/개인키에 대한 변환을, SecretKeyFactory는 대칭키(SecretKey)에 대한 변환을 담당합니다. 예를 들어 RSA 공개키의 바이트 표현을 X509EncodedKeySpec으로부터 PublicKey 객체로 복원할 수 있습니다.
Key  암호 키 객체를 추상화하는 최상위 인터페이스입니다. 공개키(PublicKey), 개인키(PrivateKey), 대칭 비밀키(SecretKey) 등이 모두 Key 인터페이스를 상속하며, 키의 알고리즘 이름, 포맷(인코딩 형식), 바이너리 인코딩을 얻는 메서드를 공통으로 제공합니다 . 이 인터페이스를 통해 상위 모듈은 키 유형에 상관없이 일관된 방식으로 키를 다룰 수 있습니다.
KeyPair 공개키와 개인키 한 쌍을 담는 단순한 컨테이너 클래스입니다. getPublic()  getPrivate() 메서드로 각 키를 꺼낼 수 있으며, 주로 키 생성 결과를 전달하거나 키 쌍을 보관하는 데 사용됩니다 .
KeyAgreement 키 합의(key agreement) 프로토콜을 구현하는 엔진 클래스입니다. 두 명 이상의 참여자가 각자의 키 정보를 교환하여 공동의 비밀키를 도출할 때 사용됩니다. 예를 들어 Diffie-Hellman 알고리즘을 통해 공유 비밀을 생성할 수 있으며, init(자신의 개인키)  doPhase(상대측 공개키) 등을 거쳐 generateSecret()으로 합의된 공유 키를 얻습니다 .
KeyStore 키 저장소를 나타내는 클래스입니다. 파일 등으로부터 키스토어를 불러오거나 생성(load 메서드), 키와 인증서를 저장(setKeyEntry)하거나 읽어오는 기능을 제공합니다. 하나의 KeyStore 내부에 여러 개의 별칭(alias)으로 키 항목을 저장할 수 있으며, 각 개인 키는 자신의 인증서 체인을 함께 보관합니다 . 또한 신뢰된 공개키 인증서들을 별도로 저장하여 신뢰 저장소로 활용할 수도 있습니다. 키스토어는 JKS(Java Key Store), PKCS#12 등 다양한 형식으로 구현되어 있으며, KeyStore.getInstance("JKS")처럼 타입을 지정해 사용할 수 있습니다 .
CertificateFactory 인증서를 생성하거나 변환하는 클래스입니다. X.509 디지털 인증서를 대표적으로 지원하며, generateCertificate(InputStream) 메서드를 통해 파일이나 바이트스트림에 인코딩된 인증서 데이터를 Certificate 객체로 파싱해낼 수 있습니다. 또한 인증서 폐기 목록(CRL) 파일을 파싱하거나, 반대로 Certificate 객체를 인코딩된 형태로 변환하는 등의 기능도 제공합니다 .

 

각 클래스는 JCA의 표준 API로서 일관된 사용 패턴과 상호작용을 보여주며, 개발자는 필요에 따라 적절한 클래스를 선택하여 보안 기능을 구현하면 됩니다. 예를 들어, 데이터를 해시해야 할 때는 MessageDigest를, 암호화가 필요할 때는 Cipher를, 서명이 필요할 때는 Signature를 사용하는 식입니다. 이러한 JCA 구조를 통해 자바 애플리케이션은 복잡한 암호 기술을 비교적 단순한 코드로 활용할 수 있고, 다양한 알고리즘과 구현체 사이에서도 호환성과 유연성을 유지할 수 있습니다. JCA는 구현과 알고리즘의 세부사항을 캡슐화한 채 표준화된 인터페이스를 제공함으로써, 자바 보안 기능의 확장성과 견고성을 뒷받침하는 기반이 됩니다.

 

 

JCA의 역할과 장점

  • 구현 독립성 – 애플리케이션 개발자는 보안 알고리즘을 직접 구현할 필요 없이 JCA가 제공하는 표준 API를 통해 필요한 보안 서비스를 요청하면 된다 . 실제 알고리즘 구현은 자바 플랫폼에 플러그인 형태로 제공되는 프로바이더들이 담당하므로, 개발자는 인터페이스만 알고 활용하면 된다 .
  • 알고리즘 독립성 및 표준화 – JCA는 다양한 알고리즘에 대해 표준화된 인터페이스를 제공하여, 코드 변경 없이도 알고리즘을 바꾸거나 업그레이드할 수 있는 유연성을 준다. 예를 들어, 해시 함수 알고리즘을 MD5에서 SHA-256으로 바꾸더라도 MessageDigest.getInstance("MD5")에서 알고리즘 이름 문자열만 SHA-256으로 변경하면 동일한 방식으로 사용할 수 있다. (완전한 의미의 알고리즘 중립성을 달성할 수는 없지만, JCA는 알고리즘별로 표준화된 API를 제공함으로써 이와 유사한 효과를 낸다 .)
  • 상호운용성 – 서로 다른 프로바이더의 구현체들도 호환되도록 설계되어, 하나의 프로바이더에서 생성한 키나 서명 등을 다른 프로바이더를 통해서도 사용할 수 있다 . 애플리케이션은 특정 프로바이더에 종속되지 않고 동작하며, 프로바이더 역시 특정 애플리케이션에 종속되지 않고 여러 애플리케이션에서 공통으로 사용될 수 있다 .
  • 확장성과 유연성 – 새로운 알고리즘이나 보안 서비스가 필요하면 JCA에 커스텀 프로바이더를 추가함으로써 손쉽게 플랫폼의 암호 기능을 확장할 수 있다 . JDK에는 널리 사용되는 기본 알고리즘들이 내장 프로바이더로 제공되지만, 표준에 없던 최신 알고리즘이나 특수한 요구가 있을 경우 별도의 프로바이더를 설치하여 지원할 수 있다 .
  • 안전성과 편의성 – JCA는 검증된 구현체들을 통합된 방식으로 제공하므로, 개발자가 저수준 구현상의 실수를 줄이고 안전한 기본값을 활용할 수 있게 한다. 또한 보안 관련 설정(예: 프로바이더 우선순위, 제한 정책 등)을 중앙 관리할 수 있어 정책 적용이 용이하다. JCA의 구조 덕분에 성능이 향상된 구현이나 보안이 강화된 구현이 나올 경우 프로바이더 교체만으로 애플리케이션을 수정 없이 개선할 수도 있다 .

 

JCA Provider 사용 예제 소스코드

import java.security.Security;
import javax.crypto.Cipher;

// 등록된 Provider 목록 출력
for (java.security.Provider provider : Security.getProviders()) {
    System.out.println(provider.getName());
}

// 특정 Provider 사용하여 Cipher 인스턴스 생성
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding", "SunJCE");

 

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

fillInStackTrace 정의

public Throwable fillInStackTrace()

 

  • 이 메서드는 Throwable 클래스에 정의되어 있으며, 모든 예외 클래스(Exception, Error)의 부모 클래스이기 때문에 예외 관련 객체에서 사용할 수 있습니다.
  • fillInStackTrace()를 호출하면 현재 위치에서의 스택 트레이스를 다시 기록합니다.
  • 일반적으로 예외를 재사용하거나, 예외를 다른 형태로 변환할 때 사용됩니다.
public class Example {
    public static void main(String[] args) {
        try {
            throwException();
        } catch (Exception e) {
            e.fillInStackTrace(); // 현재 스택 트레이스로 덮어씀
            e.printStackTrace();  // 덮어쓴 스택 트레이스를 출력
        }
    }

    static void throwException() throws Exception {
        throw new Exception("예외 발생!");
    }
}

사용 시 주의사항

  • fillInStackTrace()는 비용이 비쌀 수 있음 (스택 정보를 새로 생성).
  • 대부분의 경우, 이 메서드를 직접 호출할 필요는 없습니다.
  • 예외를 wrap할 때나, 커스텀 예외 처리에서 스택 트레이스를 재설정하고 싶을 때 사용됩니다.

 

fillInStackTrace를 통하여 스택트레이스를 제거하는 방법

class MyException extends Exception {
    public MyException(String message) {
        super(message);
    }

    @Override
    public synchronized Throwable fillInStackTrace() {
        return this; // 스택 트레이스 무시
    }
}
728x90

'Programming > JAVA' 카테고리의 다른 글

JAVA JCA(Java Cryptography Architecture)의 개념  (0) 2025.04.21
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
728x90

 

소스 코드로 작성한 C 기반 공유 라이브러리에서 어떤 함수나 전역 변수가 외부로 export되고 있는지 확인하려면, 각각의 운영체제에 맞는 도구를 사용해야 합니다. Linux의 .so(ELF 형식)와 Windows의 .dll(PE 형식)은 바이너리 포맷이 다르므로, 이를 분석하는 도구도 달라집니다. 아래에서는 CLI 도구 GUI 도구를 활용하여 각 플랫폼별로 라이브러리 내보낸 심볼 목록을 확인하는 방법을 설명합니다. 또한 빌드 시에 필요한 옵션이나 유의사항도 함께 다룹니다.

Linux – 공유 라이브러리(.so)의 심볼 확인

Linux에서 공유 라이브러리(.so)에 export된 심볼(외부에 공개된 함수나 전역변수)이 무엇인지 확인하는 대표적 CLI 도구는 nm과 objdump, readelf 등이 있습니다. 이들은 ELF 파일의 심볼 테이블을 읽어와 어떤 심볼들이 정의되어 있고(export) 또는 참조되고(import) 있는지 보여줍니다.

nm 도구 사용 (CLI)

nm은 오브젝트 파일이나 라이브러리의 심볼 목록을 출력해주는 GNU Binutils 도구입니다. 기본적으로 nm을 그냥 실행하면 정적(symbol table) 심볼까지 모두 보여주는데, 공유 라이브러리의 export 심볼만 보려면 동적 심볼 테이블만 표시하도록 -D 옵션을 주는 것이 좋습니다. nm -D는 공유 라이브러리의 동적 심볼(다이나믹 심볼 테이블에 등재된 항목)만 표시하며, 이것이 곧 외부에 노출된 API 심볼과 외부 의존 심볼을 나타냅니다. 사용 예시는 다음과 같습니다.

$ nm -D libexample.so

 

위 명령의 출력에는 각 심볼의 주소(address), 타입(code/data/undefined 등), 이름(name)이 나옵니다. 예를 들어 어떤 라이브러리가 exported_function이라는 글로벌 함수와 exported_variable이라는 글로벌 변수를 내보내고, internal_function이라는 static 함수와 internal_variable이라는 static 변수를 내부적으로만 가지고 있다고 가정해보겠습니다. 이 경우 nm -D libexample.so의 출력 예시는 대략 다음과 같습니다:

00000000 T exported_function
00000000 D exported_variable
         U printf@GLIBC_2.2.5
  • T exported_function  T는 전역(Text) 심볼로서, 라이브러리 내부에 정의된 글로벌 함수임을 나타냅니다. 즉 이 함수가 외부로 export 되어 있음을 의미합니다
  • D exported_variable  D는 전역(Data) 심볼로서, 초기값이 있는 글로벌 변수가 export되었음을 나타냅니다
  • U printf@GLIBC_2.2.5  U는 정의되지 않은(Undefined) 외부 심볼로, 이 라이브러리가 사용하는 다른 라이브러리의 함수입니다 (여기서는 printf, GLIBC로부터 가져옴). 즉 import된 심볼을 뜻하며, export 심볼과 구분됩니다.

nm 출력의 심볼 타입 코드는 대소문자로 구분되며, export 여부와 섹션을 나타냅니다. 예를 들어 대문자 T, D, B 등은 전역 심볼(외부 공개)을 의미하고, 소문자 t, d, b 등은 로컬 심볼(내부)을 의미합니다. 주요 코드의 의미는 다음과 같습니다.

  • T (Text): 전역 함수 코드 심볼 (export된 함수)
  • t (text): 지역 함수 코드 심볼 (static 함수 등, 외부 비공개)
  • D (Data): 전역 초기화 데이터 심볼 (export된 전역 변수)
  • d (data): 지역 초기화 데이터 심볼 (파일 내부 static 변수 등)
  • B (BSS): 전역 초기화되지 않은 심볼 (export된 전역 변수, 초기값 없음)
  • b (bss): 지역 초기화되지 않은 심볼 (static 전역 변수, 초기값 없음)
  • U (Undefined): 정의되어 있지 않은 심볼 (다른 객체/라이브러리에서 제공되는 함수나 변수)

nm -D로 보면 외부에 노출된 심볼들만 나타나기 때문에, 내부 static 함수나 변수들은 출력되지 않습니다 (동적 심볼 테이블에 없으므로). 만약 nm을 -D 없이 실행하면 정적 심볼 테이블까지 포함하여 모든 심볼이 출력되는데, 이 경우 static 심볼들도 보입니다. 예를 들어 nm libexample.so (옵션 없이)의 출력에 internal_function이 소문자 t로 나타난다면, 이는 해당 함수가 내부(static) 심볼이라 외부로 export되지 않았음을 알 수 있습니다. 반면 nm -D 출력에는 internal_function이 없으므로 export되지 않은 것입니다.

참고: C++로 작성된 라이브러리의 경우 nm 출력 심볼 이름이 mangled name(이름 변환) 형태일 수 있습니다. 이때 nm --demangle (또는 nm -C) 옵션이나 c++filt 도구를 사용하면 사람이 읽기 쉬운 원래의 함수 이름으로 풀어줍니다. C 코드 또는 extern "C"로 선언된 함수는 이름이 그대로 출력됩니다.

nm은 맥 OS 등 다른 Unix 계열에서도 사용 가능합니다. Mac의 .dylib에서는 nm -gU 등을 사용하지만, 여기서는 Linux ELF에 집중합니다.

objdump 도구 사용 (CLI)

objdump는 바이너리의 다양한 정보를 표시할 수 있는 만능 도구입니다. 심볼 목록을 보려면 objdump에 적절한 옵션을 주면 됩니다. 두 가지 방식이 있습니다.

  • objdump -t libexample.so : 정적 심볼 테이블(symbol table)의 모든 심볼을 표시합니다 (nm 기본 출력과 유사). 여기엔 로컬 심볼까지 모두 나오지만, 보통 공유 라이브러리는 릴리즈 빌드시 불필요한 심볼을 strip하기 때문에 -t 출력이 없을 수도 있습니다.
  • objdump -T libexample.so : 동적 심볼 테이블을 표시합니다. -T는 ELF의 .dynsym 섹션을 덤프하며, 이는 nm -D와 마찬가지로 export된 심볼과 import된 외부 참조만 보여줍니다

예를 들어 objdump -T libexample.so의 출력은 다음과 같은 컬럼을 가집니다.

$ objdump -T libexample.so

00000000 g    DF .text  0000003d  Base    exported_function
00000000 g    DO .data  00000004  Base    exported_variable
...
  • 앞의 주소와 함께 g DF .text처럼 표시되는데, g는 글로벌, DF는 FUNC (함수), .text는 코드 섹션에 정의됨을 뜻합니다. 즉 exported_function이 전역 함수로 export되었음을 보여줍니다.
  • DO .data는 OBJECT (데이터 객체)를 의미하며 .data 섹션에 있는 전역 변수 심볼임을 나타냅니다. exported_variable이 전역 변수로 export되었다는 뜻입니다.
  • Base는 심볼 버전(version)이 없는 기본 심볼임을 나타냅니다 (라이브러리에 버전 스크립트를 사용한 경우 해당 심볼의 버전 정보가 표시될 수 있습니다).

이처럼 objdump -T는 nm -D와 유사한 정보를 제공하지만, 심볼의 크기나 섹션 같은 추가 정보도 보여줍니다. 만약 자세한 헤더와 섹션 정보까지 보고 싶다면 objdump -x libexample.so (파일 헤더와 섹션 헤더 등 모두 출력)로 전체 내용을 볼 수 있습니다

readelf 도구 사용 (CLI)

readelf는 ELF 파일 구조를 해석해주는 도구입니다. 심볼 정보를 보려면 readelf -s 옵션을 사용합니다. 기본적으로 readelf -s libexample.so는 심볼 테이블을 모두 보여주는데, 공유 라이브러리의 경우 정적 심볼 테이블(.symtab)과 동적 심볼 테이블(.dynsym)이 모두 있을 수 있습니다. 동적 심볼만 보려면 readelf --dyn-syms -s libexample.so 또는 줄여서 readelf -Ds libexample.so 명령을 사용할 수 있습니다 출력 형식은 nm보다 상세하여, 각 심볼의 인덱스, 값, 크기, 타입(Func/Object), 바인딩(Global/Local), 섹션, 이름 등이 테이블 형태로 나옵니다. Global 바인딩의 FUNC/OBJECT가 곧 export된 함수/변수이고, UND로 표시된 것은 undefined (import) 심볼입니다.

예를 들어 readelf -Ws libexample.so의 출력 일부는 다음과 같을 수 있습니다 (열을 간략화하여 표시):

   Num: Value    Size  Type    Bind   Vis      Ndx Name
   10: 0000111b  0x3d  FUNC    GLOBAL DEFAULT   13 exported_function
   11: 00004010  0x4   OBJECT  GLOBAL DEFAULT   25 exported_variable
   12: 00000000    0   FUNC    GLOBAL DEFAULT  UND printf@GLIBC_2.2.5

여기서 Bind GLOBAL이고 Ndx (섹션 인덱스)가 UND가 아닌 항목들이 바로 이 라이브러리에서 제공하는 export 심볼입니다.

readelf의 출력은 기계적으로 정확하지만 다소 장황할 수 있으므로, 간단히는 nm이나 objdump 출력만으로도 충분합니다.

Linux에서의 빌드 옵션과 심볼 가시성

Linux의 공유 라이브러리는 별다른 지시 없이 컴파일하면 모든 전역 함수/변수가 기본적으로 export됩니다. 즉, static으로 선언하지 않은 함수는 특별히 숨기지 않는 한 동적 심볼 테이블에 나타나게 됩니다. 경우에 따라 내부 구현 함수까지 외부에 노출되는 것을 원하지 않을 수 있는데, 이런 불필요한 심볼 노출을 막기 위해 visibility를 제어할 수 있습니다. GCC에서는 -fvisibility=hidden 컴파일 옵션을 주면 기본적으로 모든 전역 심볼을 숨기고, 필요한 심볼만 선택적으로 export할 수 있습니다. 예를 들어 컴파일/링크 옵션에 -fvisibility=hidden을 추가하고, 외부에 공개해야 할 함수나 변수 선언에 __attribute__((visibility("default")))를 붙이면 해당 심볼만 외부로 공개되고 나머지는 숨겨집니다 .

이 기법을 사용하면 nm -D 출력에서 공개하지 않은 내부 함수들은 사라지고, 설계한 API만 export됩니다. (참고로, GCC 4.x 이후로는 라이브러리 제작 시 이 방법이 권장되어, 불필요한 심볼을 줄이고 DSO 로딩 시간을 개선하는 등 이점이 있습니다.) 예시:

// 헤더 파일 선언부
__attribute__((visibility("default"))) void public_func();
// 구현 파일
void public_func() { /* ... */ }       // 공개함수: default visibility
static void internal_util() { /* ... */ }  // 내부함수: hidden (static이거나 visibility hidden)

이렇게 빌드하면 nm -D에 public_func만 나타나고 internal_util은 나타나지 않게 됩니다. (동일 효과를 링커의 버전 스크립트로도 낼 수 있지만, 컴파일 속성을 사용하는 방법이 더 간편합니다.)

Windows – DLL(.dll)의 심볼 확인

Windows DLL에서는 어떤 함수들이 외부로 export되었는지 확인하는 방법으로 CLI 도구 GUI 도구를 모두 사용할 수 있습니다. Windows에서는 기본적으로 DLL을 빌드할 때 개발자가 export 대상을 지정해주지 않으면 아무 심볼도 export되지 않는다는 점이 Linux와 다릅니다. 따라서 Windows DLL의 내보낸 심볼 목록을 확인하는 것은 곧, 빌드 시 설정한 __declspec(dllexport)나 .def 파일에 의해 지정된 API가 잘 포함되었는지를 확인하는 과정입니다.

대표적인 CLI 도구로는 Visual Studio에 포함된 dumpbin이 있고, GUI 도구로는 Dependency Walker 등이 널리 사용됩니다.

dumpbin 도구 사용 (CLI)

Microsoft의 Visual C++ 툴체인에는 dumpbin이라는 실행 파일이 포함되어 있는데, 이를 이용하면 DLL(또는 LIB)의 정보를 덤프할 수 있습니다. Developer Command Prompt (개발자 명령 프롬프트)에서 dumpbin /EXPORTS 파일명.dll 명령을 실행하면 해당 DLL의 내보낸 심볼 목록을 볼 수 있습니다. 사용 순서는 다음과 같습니다.

  1. Visual Studio에서 개발자 명령 프롬프트를 엽니다 (VS2019/2022의 경우 Tools > Command Line > Developer Command Prompt 메뉴 등으로 실행).
  2. DLL 파일이 있는 디렉토리로 이동하여, 명령어 dumpbin /exports MyLibrary.dll를 실행합니다.
  3. 출력 결과를 확인합니다.

예를 들어 MyLibrary.dll에 대해 dumpbin /exports MyLibrary.dll을 실행하면 다음과 같은 형식의 출력이 나타납니다:

Dump of file MyLibrary.dll

File Type: DLL

  Section contains the following exports for MyLibrary.dll

    00000000 characteristics
    00000000 time date stamp
        0.00 version
        1 ordinal base
        3 number of functions
        3 number of names

    ordinal hint   RVA      name
          1    0   00001000 MyFunction
          2    1   00001030 MyVariable
          3    2   00002000 _DllMainCRTStartup@12

위 출력에서 중요한 부분은 마지막 표 형태입니다. 각 열의 의미는 다음과 같습니다.

  • ordinal: 내보낸 심볼의 서수(Ordinal) 값입니다. DLL에서 심볼마다 고유한 번호가 부여되며, import 라이브러리가 이 번호로 참조할 수도 있습니다. Ordinal base가 1이면 첫 번째 심볼의 ordinal이 1부터 시작함을 뜻합니다.
  • hint: 심볼 이름 검색을 빠르게 하기 위한 힌트 인덱스입니다. (보통 내부용이라 크게 신경쓰지 않아도 됩니다.)
  • RVA: 해당 심볼의 Relative Virtual Address (상대 가상 주소)로, DLL 로드 시 메모리에서의 오프셋 주소를 나타냅니다. 즉, DLL 시작을 기준으로 함수 코드가 있는 주소 값입니다.
  • name: 내보낸 심볼 이름입니다. 함수 이름 또는 변수 이름이 표시됩니다. C로 export된 함수는 그대로 이름이 나오지만, __stdcall 규약을 사용했다면 이름 뒤에 @<스택 바이트> 형식으로 표기되거나, C++ 함수라면 맹글링된 이름이 보일 수 있습니다. 예를 들어 _DllMainCRTStartup@12는 DLL 엔트리 포인트로, stdcall 규약에 따라 추가 장식이 된 이름입니다.

dumpbin /exports 결과에서 함수명/변수명 목록을 보고, 내가 의도한 함수들이 잘 export되었는지 확인할 수 있습니다. 만약 리스트에 없다면, 빌드 과정에서 내보내기 선언을 빼먹은 것입니다. C++ 함수가 목록에 있을 경우 맹글링된 이름 (예: ?FuncName@@YAXH@Z)으로 표시될 수 있는데, 이는 Visual C++의 장식 규칙에 따른 것입니다. 이런 경우 DLL을 배포할 때 C 인터페이스로 extern "C"를 사용하거나 .def 파일을 통해 이름을 지정하는 방식으로 이름 장식 문제를 해결해야 합니다.

참고: Visual Studio가 없다면, GNU Binutils의 objdump로도 Windows DLL의 export를 확인할 수 있습니다. 예를 들어 Linux 환경에서 objdump -p MyLibrary.dll를 실행하면 PE 포맷의 내보낸 심볼 (.edata 섹션) 정보를 읽을 수 있습니다. 출력에서 [Ordinal/Name Pointer] Table 아래에 함수 목록이 표시되며, Ordinal과 Name 열을 통해 export 심볼을 파악할 수 있습니다. 하지만 이 방법은 출력 해석이 다소 불편하므로, 일반적으로 Windows에서는 dumpbin이나 전용 GUI 툴을 사용하는 편이 낫습니다.

Dependency Walker 사용 (GUI)

Dependency Walker(depends.exe)는 Windows DLL의 의존성 내보낸/불러오는 심볼을 그래픽 인터페이스로 보여주는 오래된 도구입니다. 마이크로소프트 공식 툴은 아니지만 개발자 사이에 널리 사용됩니다. 사용 방법은 다음과 같습니다.

  1. Dependency Walker 프로그램(depends.exe)을 실행합니다. (공식 웹사이트에서 다운로드한 후 별도 설치 없이 실행 가능)
  2. 메뉴에서 File > Open 을 클릭하고 검사하고자 하는 DLL 파일(MyLibrary.dll)을 엽니다.
  3. 잠시 분석이 진행된 후, 화면에 해당 DLL의 내보낸 함수 목록이 표시됩니다. 보통 상단 창에 “Exports”로 불리는 목록이 나타나고, 하단 창에는 해당 DLL이 의존하는 다른 DLL들(Imports)이 나열됩니다.
  4. 상단 Exports 창에서 함수 이름, ordinal, 그리고 Hint 등을 확인할 수 있습니다. Dependency Walker는 dumpbin과 유사한 정보를 보여주지만, GUI로 제공하므로 스크롤과 정렬이 가능합니다.

예를 들어 MyLibrary.dll을 열었을 때 Dependency Walker의 Export 창에 MyFunction, MyVariable 등이 나타나면, 해당 심볼들이 외부로 export되었음을 쉽게 확인할 수 있습니다. 만약 의도한 함수가 리스트에 없으면, 앞서 언급한 대로 빌드 설정을 확인해야 합니다.

추가 도구: 이 외에도 DLL Export Viewer(NirSoft 제공)와 같은 GUI 툴도 있습니다. Dependency Walker와 비슷하게 DLL을 열면 export 심볼을 나열해주며, 좀 더 현대적인 Windows에서도 동작합니다. 그러나 기본 원리는 동일하므로 여기서는 자세한 설명을 생략합니다.

Windows에서의 빌드 옵션과 export 설정

Windows에서는 DLL을 생성할 때 어떤 심볼을 export할지 명시적으로 지정해야 합니다. 기본적으로 __declspec(dllexport) 키워드를 사용하여 함수나 변수를 선언하면, 컴파일 시 해당 심볼이 DLL의 export 테이블에 추가됩니다. 반대로 DLL을 사용하는 쪽에서는 __declspec(dllimport)를 통해 import 라이브러리 사용을 명시하지요. 간단한 예를 들면:

// dllheader.h (DLL의 공개 헤더)
__declspec(dllexport) int myFunction(int x);
// dll.c (DLL 구현)
__declspec(dllexport) int myFunction(int x) { return x * 2; }
static int internalUtil(int y) { return y * 3; }

위 코드에서 myFunction은 __declspec(dllexport)로 선언되었으므로 DLL의 내보낸 심볼 테이블에 포함됩니다. 반면 internalUtil은 static이며 dllexport가 없으므로 외부에 노출되지 않습니다. 이 DLL을 빌드한 뒤 dumpbin /exports나 Dependency Walker로 확인해보면 myFunction만 export 목록에 나타나고 internalUtil은 보이지 않게 됩니다.

만약 __declspec(dllexport)를 쓰지 않고 DLL을 빌드하면, 기본적으로 export되는 심볼이 없습니다. (예외적으로 DllMain처럼 시스템이 암묵적으로 참조하는 엔트리 포인트나, C++의 extern "C"로 내보낸 객체를 .def 파일에 기재한 경우 등이 있지만, 일반적인 C 코드에서는 지정하지 않는 한 export되지 않습니다.) 따라서 Windows DLL에서 API를 제공하려면 반드시 헤더에 export할 함수/변수를 __declspec(dllexport)로 선언하거나, 모듈 정의 파일(.def)의 EXPORTS 절에 해당 이름을 나열해야 합니다. .def 파일을 사용하는 방법은 대규모 프로젝트에서 유용하며, 이를 사용하면 소스코드에 컴파일 지시문을 넣지 않고도 export 심볼을 관리할 수 있습니다. .def 파일이 사용된 경우에도 dumpbin /exports 출력에 해당 함수들이 나타납니다 (이 때 함수 이름 옆에 ordinal만 지정되고 hint는 0으로 표시되는 등 출력 형태만 약간 다릅니다).

마지막으로, C++ 함수를 DLL에서 export할 때는 name mangling에 주의해야 합니다. C++ 함수명을 그대로 export하면 dumpbin이나 Dependency Walker에 맹글된 이름이 보이고, 호출 시에도 그 맹글된 이름을 알아야 합니다. 이를 피하려면 함수 선언을 extern "C"로 감싸서 C링케이지로 만들거나, .def 파일에서 Alias를 지정하여 이름을 변경할 수 있습니다. 일반적으로 DLL의 공개 API는 C 인터페이스로 구성하거나, C++ 클래스라도 공용 API에는 C 방식의 래퍼를 제공하기도 합니다.

 

728x90

+ Recent posts