C++ 연동

Prev Next

Hardware Security Module(HSM) 상품은 Thales Luna HSM을 기반으로 하며, PKCS#11(Cryptoki) C/C++ API로 사용할 수 있습니다. 이 문서에서는 C++ 애플리케이션에서 Cryptoki API로 HSM 파티션에 접속해 AES/RSA 같은 기본 알고리즘으로 키 생성, 암호화·복호화, 서명·검증을 수행하는 최소 예제를 안내합니다.

참고

CLI로 먼저 만져보려면 CLI 도구 활용 (lunacm/ckdemo/multitoken)을 참고해 주십시오.
LunaClient를 설치하면 p11Sample(ckSample 패키지) 소스 예제도 함께 설치됩니다(보통 /usr/safenet/lunaclient/sdk/samples/ 아래). 이 문서의 dlopen/dlsym 패턴도 이 샘플을 기반으로 하며, 더 다양한 예제가 필요하면 먼저 이 샘플을 찾아보십시오.

지원 환경

지원 환경을 설명합니다.

전제 조건

연동을 시작하기 전, 다음의 전제 조건을 준수해 주십시오.

  • HSM 클라이언트 서버에 C/C++ 빌드 도구(g++)가 설치되어 있음
  • HSM 클라이언트 서버에 Luna HSM Client(LunaClient) 10.9.3이 설치되어 있음 — LunaClient 설치 가이드
  • HSM 클라이언트 서버 인스턴스와 HSM 파티션 간에 연결 생성 완료
  • HSM 파티션 초기 설정 완료

지원 버전

구성 요소 버전 비고
HSM 클라이언트(Luna Client) 10.9.3 LunaClient 설치 가이드

빌드 방법

Cryptoki 라이브러리(libCryptoki2_64.so)는 컴파일 시점에 링크하지 않고, 런타임에 dlopen으로 불러와 사용합니다. 아래 플래그로 컴파일하십시오.

g++ -DUNIX -DOS_UNIX \
    -I/usr/safenet/lunaclient/sdk/include \
    -I/usr/safenet/lunaclient/sdk/external \
    -o aes_example aes_example.cpp \
    -ldl

다른 예제도 소스 파일명만 바꾸면 동일합니다.

세션 초기화 및 파티션 로그인

아래는 모든 예제 코드에 공통으로 포함되는 부분입니다. Cryptoki 라이브러리를 dlopen으로 불러오고, 세션을 열어 파티션에 로그인합니다.

#include "cryptoki_v2.h"
#include <dlfcn.h>
#include <cstdio>
#include <cstring>
#include <cstdlib>

// libCryptoki2_64.so를 런타임에 로드하고 함수 포인터 테이블(p11)을 가져온다
const char *libPath = getenv("SfntLibPath");
void *libHandle = dlopen(libPath, RTLD_NOW);
CK_C_GetFunctionList C_GetFunctionList = (CK_C_GetFunctionList)dlsym(libHandle, "C_GetFunctionList");
CK_FUNCTION_LIST_PTR p11 = NULL;
C_GetFunctionList(&p11);

// 세션 오픈 및 파티션 로그인
CK_SESSION_HANDLE hSession;
p11->C_Initialize(NULL_PTR);
p11->C_OpenSession(slotId, CKF_SERIAL_SESSION | CKF_RW_SESSION, NULL_PTR, NULL_PTR, &hSession);
p11->C_Login(hSession, CKU_USER, pin, (CK_ULONG)strlen((char *)pin));

실행 전 라이브러리 경로를 SfntLibPath 환경변수로 지정해야 합니다.

export SfntLibPath=/usr/safenet/lunaclient/lib/libCryptoki2_64.so

슬롯 번호는 lunacm:> slot list로 확인할 수 있습니다. (CLI 도구 활용 참고)

AES 키 생성 및 암호화·복호화

AES 키는 CKM_AES_KEY_GEN mechanism으로 생성하고, 암호화·복호화는 CKM_AES_CBC_PAD mechanism을 사용합니다. aes_example.cpp 파일을 아래 내용으로 생성하십시오.

#include "cryptoki_v2.h"
#include <dlfcn.h>
#include <cstdio>
#include <cstring>
#include <cstdlib>

int main() {
    // 실제 파티션의 슬롯 번호와 비밀번호로 교체하십시오.
    CK_SLOT_ID slotId = 0;
    CK_UTF8CHAR_PTR pin = (CK_UTF8CHAR_PTR)"{partition-password}";

    const char *libPath = getenv("SfntLibPath");
    void *libHandle = dlopen(libPath, RTLD_NOW);
    CK_C_GetFunctionList C_GetFunctionList = (CK_C_GetFunctionList)dlsym(libHandle, "C_GetFunctionList");
    CK_FUNCTION_LIST_PTR p11 = NULL;
    CK_RV rv = C_GetFunctionList(&p11);

    CK_SESSION_HANDLE hSession;
    p11->C_Initialize(NULL_PTR);
    p11->C_OpenSession(slotId, CKF_SERIAL_SESSION | CKF_RW_SESSION, NULL_PTR, NULL_PTR, &hSession);
    rv = p11->C_Login(hSession, CKU_USER, pin, (CK_ULONG)strlen((char *)pin));
    if (rv != CKR_OK) {
        fprintf(stderr, "C_Login failed: 0x%lx\n", rv);
        return 1;
    }

    // --- AES-256 키 생성 (CKA_TOKEN을 지정하지 않아 기본값(CK_FALSE)인 세션 객체로 생성됨) ---
    CK_BBOOL ckTrue = CK_TRUE;
    CK_ULONG keyLen = 32;
    CK_ATTRIBUTE aesKeyTemplate[] = {
        { CKA_SENSITIVE, &ckTrue, sizeof(ckTrue) },
        { CKA_ENCRYPT,   &ckTrue, sizeof(ckTrue) },
        { CKA_DECRYPT,   &ckTrue, sizeof(ckTrue) },
        { CKA_VALUE_LEN, &keyLen, sizeof(keyLen) },
    };

    CK_MECHANISM keyGenMechanism = { CKM_AES_KEY_GEN, NULL_PTR, 0 };
    CK_OBJECT_HANDLE hAesKey;
    rv = p11->C_GenerateKey(hSession, &keyGenMechanism,
                             aesKeyTemplate, sizeof(aesKeyTemplate) / sizeof(CK_ATTRIBUTE),
                             &hAesKey);
    if (rv != CKR_OK) {
        fprintf(stderr, "C_GenerateKey failed: 0x%lx\n", rv);
        return 1;
    }

    // --- 암호화 ---
    CK_BYTE iv[16];
    p11->C_GenerateRandom(hSession, iv, sizeof(iv));
    CK_MECHANISM aesMechanism = { CKM_AES_CBC_PAD, iv, sizeof(iv) };

    CK_BYTE plaintext[] = "HSM C++ Quickstart test data";
    CK_ULONG plaintextLen = sizeof(plaintext) - 1;

    p11->C_EncryptInit(hSession, &aesMechanism, hAesKey);
    CK_ULONG encryptedLen = 0;
    p11->C_Encrypt(hSession, plaintext, plaintextLen, NULL_PTR, &encryptedLen);
    CK_BYTE_PTR encrypted = (CK_BYTE_PTR)malloc(encryptedLen);
    rv = p11->C_Encrypt(hSession, plaintext, plaintextLen, encrypted, &encryptedLen);
    if (rv != CKR_OK) {
        fprintf(stderr, "C_Encrypt failed: 0x%lx\n", rv);
        free(encrypted);
        return 1;
    }

    // --- 복호화 (같은 IV로 재초기화) ---
    p11->C_DecryptInit(hSession, &aesMechanism, hAesKey);
    CK_ULONG decryptedLen = 0;
    p11->C_Decrypt(hSession, encrypted, encryptedLen, NULL_PTR, &decryptedLen);
    CK_BYTE_PTR decrypted = (CK_BYTE_PTR)malloc(decryptedLen);
    rv = p11->C_Decrypt(hSession, encrypted, encryptedLen, decrypted, &decryptedLen);
    if (rv != CKR_OK) {
        fprintf(stderr, "C_Decrypt failed: 0x%lx\n", rv);
        free(encrypted);
        free(decrypted);
        return 1;
    }

    bool matches = (decryptedLen == plaintextLen) && (memcmp(plaintext, decrypted, plaintextLen) == 0);
    printf("Decryption matches original: %s\n", matches ? "true" : "false");

    free(encrypted);
    free(decrypted);
    p11->C_Logout(hSession);
    p11->C_CloseSession(hSession);
    p11->C_Finalize(NULL_PTR);
    dlclose(libHandle);
    return 0;
}

빌드 방법에 따라 aes_example로 빌드한 뒤 실행하십시오.

export SfntLibPath=/usr/safenet/lunaclient/lib/libCryptoki2_64.so
./aes_example

다음과 같은 결과가 출력되면 정상입니다.

Decryption matches original: true

생성한 키를 다음 실행에서도 다시 쓰려면

위 예제처럼 CKA_TOKEN을 지정하지 않고 생성한 키는 세션(session) 객체입니다(생략 시 기본값은 CK_FALSE). 세션(프로그램)이 끝나면 HSM에서도 함께 사라지므로, 다음번 실행에서 같은 키를 다시 쓸 수 없습니다.

같은 키를 다음 실행에서도 찾아 쓰려면, 생성 시점에 CKA_TOKEN = CK_TRUE와 나중에 찾을 수 있는 CKA_LABEL을 템플릿에 추가해 영구(token) 객체로 만들면 됩니다. Java의 KeyStore.setKeyEntry 같은 별도 API 호출은 필요 없습니다.

CK_BBOOL ckTrue = CK_TRUE;
CK_UTF8CHAR label[] = "my-aes-key";
CK_ULONG keyLen = 32;
CK_ATTRIBUTE aesKeyTemplate[] = {
    { CKA_TOKEN,     &ckTrue, sizeof(ckTrue) },    // 영구(token) 객체로 생성
    { CKA_LABEL,     label,   sizeof(label) - 1 }, // 나중에 이 label로 찾음
    { CKA_SENSITIVE, &ckTrue, sizeof(ckTrue) },
    { CKA_ENCRYPT,   &ckTrue, sizeof(ckTrue) },
    { CKA_DECRYPT,   &ckTrue, sizeof(ckTrue) },
    { CKA_VALUE_LEN, &keyLen, sizeof(keyLen) },
};

다음 실행(또는 다른 프로세스)에서는 새로 생성하지 않고, CKA_LABEL로 검색(C_FindObjectsInit/C_FindObjects)해서 같은 키의 핸들을 다시 얻습니다.

CK_UTF8CHAR label[] = "my-aes-key";
CK_ATTRIBUTE findTemplate[] = {
    { CKA_LABEL, label, sizeof(label) - 1 },
};

CK_OBJECT_HANDLE hAesKey;
CK_ULONG foundCount = 0;
p11->C_FindObjectsInit(hSession, findTemplate, 1);
p11->C_FindObjects(hSession, &hAesKey, 1, &foundCount);
p11->C_FindObjectsFinal(hSession);

if (foundCount == 0) {
    fprintf(stderr, "Key not found\n");
    return 1;
}
참고

CKA_TOKEN = CK_TRUE로 만든 키는 프로그램이 끝나도 파티션에 남습니다. 테스트하며 여러 번 만들었다면 ckdemo의 Object Management 메뉴(22, Destroy object)로 정리해 주십시오.

RSA 키 생성, 서명 및 검증

RSA 키쌍은 CKM_RSA_PKCS_KEY_PAIR_GEN mechanism으로 생성하고, 서명·검증은 CKM_SHA256_RSA_PKCS mechanism을 사용합니다. rsa_signature_example.cpp 파일을 아래 내용으로 생성하십시오.

#include "cryptoki_v2.h"
#include <dlfcn.h>
#include <cstdio>
#include <cstring>
#include <cstdlib>

int main() {
    // 실제 파티션의 슬롯 번호와 비밀번호로 교체하십시오.
    CK_SLOT_ID slotId = 0;
    CK_UTF8CHAR_PTR pin = (CK_UTF8CHAR_PTR)"{partition-password}";

    const char *libPath = getenv("SfntLibPath");
    void *libHandle = dlopen(libPath, RTLD_NOW);
    CK_C_GetFunctionList C_GetFunctionList = (CK_C_GetFunctionList)dlsym(libHandle, "C_GetFunctionList");
    CK_FUNCTION_LIST_PTR p11 = NULL;
    CK_RV rv = C_GetFunctionList(&p11);

    CK_SESSION_HANDLE hSession;
    p11->C_Initialize(NULL_PTR);
    p11->C_OpenSession(slotId, CKF_SERIAL_SESSION | CKF_RW_SESSION, NULL_PTR, NULL_PTR, &hSession);
    rv = p11->C_Login(hSession, CKU_USER, pin, (CK_ULONG)strlen((char *)pin));
    if (rv != CKR_OK) {
        fprintf(stderr, "C_Login failed: 0x%lx\n", rv);
        return 1;
    }

    // --- RSA-2048 키쌍 생성 (CKA_TOKEN을 지정하지 않아 기본값(CK_FALSE)인 세션 객체로 생성됨) ---
    CK_BBOOL ckTrue = CK_TRUE;
    CK_ULONG modulusBits = 2048;
    CK_BYTE publicExponent[] = { 0x01, 0x00, 0x01 };  // 65537

    CK_ATTRIBUTE publicKeyTemplate[] = {
        { CKA_VERIFY,          &ckTrue,         sizeof(ckTrue) },
        { CKA_MODULUS_BITS,    &modulusBits,    sizeof(modulusBits) },
        { CKA_PUBLIC_EXPONENT, publicExponent,  sizeof(publicExponent) },
    };
    CK_ATTRIBUTE privateKeyTemplate[] = {
        { CKA_PRIVATE,   &ckTrue, sizeof(ckTrue) },
        { CKA_SENSITIVE, &ckTrue, sizeof(ckTrue) },
        { CKA_SIGN,      &ckTrue, sizeof(ckTrue) },
    };

    CK_MECHANISM keyGenMechanism = { CKM_RSA_PKCS_KEY_PAIR_GEN, NULL_PTR, 0 };
    CK_OBJECT_HANDLE hPublicKey, hPrivateKey;
    rv = p11->C_GenerateKeyPair(hSession, &keyGenMechanism,
                                 publicKeyTemplate, sizeof(publicKeyTemplate) / sizeof(CK_ATTRIBUTE),
                                 privateKeyTemplate, sizeof(privateKeyTemplate) / sizeof(CK_ATTRIBUTE),
                                 &hPublicKey, &hPrivateKey);
    if (rv != CKR_OK) {
        fprintf(stderr, "C_GenerateKeyPair failed: 0x%lx\n", rv);
        return 1;
    }

    // --- 서명 ---
    CK_MECHANISM signMechanism = { CKM_SHA256_RSA_PKCS, NULL_PTR, 0 };
    CK_BYTE message[] = "HSM C++ Quickstart signature test";
    CK_ULONG messageLen = sizeof(message) - 1;

    rv = p11->C_SignInit(hSession, &signMechanism, hPrivateKey);
    if (rv != CKR_OK) {
        fprintf(stderr, "C_SignInit failed: 0x%lx\n", rv);
        return 1;
    }

    CK_ULONG signatureLen = 0;
    p11->C_Sign(hSession, message, messageLen, NULL_PTR, &signatureLen);

    CK_BYTE_PTR signature = (CK_BYTE_PTR)malloc(signatureLen);
    rv = p11->C_Sign(hSession, message, messageLen, signature, &signatureLen);
    if (rv != CKR_OK) {
        fprintf(stderr, "C_Sign failed: 0x%lx\n", rv);
        free(signature);
        return 1;
    }
    printf("Signature generated (%lu bytes)\n", (unsigned long)signatureLen);

    // --- 검증 ---
    CK_MECHANISM verifyMechanism = { CKM_SHA256_RSA_PKCS, NULL_PTR, 0 };
    rv = p11->C_VerifyInit(hSession, &verifyMechanism, hPublicKey);
    if (rv != CKR_OK) {
        fprintf(stderr, "C_VerifyInit failed: 0x%lx\n", rv);
        free(signature);
        return 1;
    }

    rv = p11->C_Verify(hSession, message, messageLen, signature, signatureLen);
    if (rv == CKR_OK) {
        printf("Signature verification succeeded\n");
    } else if (rv == CKR_SIGNATURE_INVALID) {
        printf("Signature verification failed - signature is invalid\n");
    } else {
        fprintf(stderr, "C_Verify failed: 0x%lx\n", rv);
    }

    free(signature);
    p11->C_Logout(hSession);
    p11->C_CloseSession(hSession);
    p11->C_Finalize(NULL_PTR);
    dlclose(libHandle);
    return 0;
}

빌드 방법에 따라 rsa_signature_example로 빌드한 뒤 실행하십시오.

export SfntLibPath=/usr/safenet/lunaclient/lib/libCryptoki2_64.so
./rsa_signature_example

다음과 같은 결과가 출력되면 정상입니다(RSA-2048 서명은 256바이트 고정 길이).

Signature generated (256 bytes)
Signature verification succeeded

문제 해결

로그인 실패

C_Login failed: 0x... (CKR_PIN_INCORRECT 등) 오류가 발생합니다.

원인

pin 변수를 실제 파티션 비밀번호로 바꾸지 않았거나, 값이 틀린 경우 발생합니다. {partition-password} placeholder를 실제 값으로 교체하십시오.

dlopen 실패

dlopen(...) failed 또는 컴파일 시 헤더를 찾을 수 없습니다.

원인

LunaClient가 설치되어 있지 않거나, 빌드 방법의 경로가 실제 설치 경로와 다른 경우 발생합니다.

해결 방법

LunaClient 10.9.3 설치 여부를 확인하십시오.

연결 오류

세션은 열리는데 로그인 이후 응답이 없거나 연결 오류가 발생합니다.

원인

HSM 클라이언트 서버와 HSM 파티션 간의 네트워크 연결(Connection)이나 파티션 초기 설정이 완료되지 않은 경우 발생할 수 있습니다.

해결 방법

전제 조건의 HSM 커넥션 생성·파티션 초기 설정이 완료됐는지 확인하십시오.

다음 단계