Documentation Index

Fetch the complete documentation index at: https://guide-fin.ncloud-docs.com/llms.txt

Use this file to discover all available pages before exploring further.

C++ 예제

Prev Next

Ncloud Hardware Security Module(HSM) 서비스는 Thales Luna HSM을 기반으로 하며, NIST 표준 양자내성암호(PQC) 알고리즘인 ML-KEM(FIPS 203, 키 캡슐화), ML-DSA (FIPS 204, 전자서명), HSS/LMS (NIST SP 800-208, RFC 8554, 상태 유지형 전자서명)를 PKCS#11(Cryptoki) C/C++ API로 지원합니다.

이 문서에서는 C++ 애플리케이션에서 Cryptoki API로 PQC 키를 생성하고, 키 캡슐화(KEM)·전자서명을 수행하는 방법을 안내합니다.

참고

ML-KEM/ML-DSA는 Java 예제 가이드로도 사용할 수 있습니다. 전체 PQC 기능 목록은 양자내성암호(PQC) — 개요를 참고해 주십시오.

이 문서의 mechanism/attribute 명세는 Thales 공식 문서를 기준으로 작성되었습니다.

지원 환경

전제 조건

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

  • 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 hss_keygen_example hss_keygen_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

ML-KEM 키 생성 및 키 캡슐화(KEM)

ML-KEM 키 쌍은 CKM_ML_KEM_KEY_PAIR_GEN mechanism으로 생성하고, CKA_PARAMETER_SET으로 파라미터 세트(512/768/1024)를 지정합니다. 캡슐화·역캡슐화는 C_EncapsulateKey/C_DecapsulateKey(PKCS#11 3.2 정식 표준)가 아직 정식 도입되기 전이라, Thales 독자 함수인 CA_EncapsulateKey/CA_DecapsulateKey를 사용합니다. 이 두 함수는 CK_FUNCTION_LIST(함수 포인터 테이블)에 없으므로 dlsym으로 직접 심볼을 가져와야 합니다. mlkem_example.cpp 파일을 아래 내용으로 생성하십시오.

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

// CA_EncapsulateKey/CA_DecapsulateKey는 함수 테이블에 없어 dlsym으로 직접 가져와야 하므로 타입을 직접 선언
typedef CK_RV (*CA_EncapsulateKeyFunc)(CK_SESSION_HANDLE, CK_MECHANISM_PTR, CK_OBJECT_HANDLE,
                                        CK_ATTRIBUTE_PTR, CK_ULONG, CK_BYTE_PTR, CK_ULONG_PTR,
                                        CK_OBJECT_HANDLE_PTR);
typedef CK_RV (*CA_DecapsulateKeyFunc)(CK_SESSION_HANDLE, CK_MECHANISM_PTR, CK_OBJECT_HANDLE,
                                        CK_ATTRIBUTE_PTR, CK_ULONG, CK_BYTE_PTR, CK_ULONG,
                                        CK_OBJECT_HANDLE_PTR);

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);

    CA_EncapsulateKeyFunc CA_EncapsulateKey = (CA_EncapsulateKeyFunc)dlsym(libHandle, "CA_EncapsulateKey");
    CA_DecapsulateKeyFunc CA_DecapsulateKey = (CA_DecapsulateKeyFunc)dlsym(libHandle, "CA_DecapsulateKey");

    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;
    }

    // --- ML-KEM 키 쌍 생성 ---
    CK_ULONG parameterSet = CKP_ML_KEM_768;  // 512/768/1024 중 선택
    CK_BBOOL ckTrue = CK_TRUE;

    // 공개키는 캡슐화(Encapsulate)용, 개인키는 역캡슐화(Decapsulate)용으로 속성을 지정
    CK_ATTRIBUTE publicKeyTemplate[] = {
        { CKA_TOKEN,         &ckTrue,       sizeof(ckTrue) },
        { CKA_PRIVATE,       &ckTrue,       sizeof(ckTrue) },
        { CKA_ENCAPSULATE,   &ckTrue,       sizeof(ckTrue) },
        { CKA_PARAMETER_SET, &parameterSet, sizeof(parameterSet) },
    };
    CK_ATTRIBUTE privateKeyTemplate[] = {
        { CKA_TOKEN,         &ckTrue,       sizeof(ckTrue) },
        { CKA_PRIVATE,       &ckTrue,       sizeof(ckTrue) },
        { CKA_SENSITIVE,     &ckTrue,       sizeof(ckTrue) },
        { CKA_DECAPSULATE,   &ckTrue,       sizeof(ckTrue) },
        { CKA_PARAMETER_SET, &parameterSet, sizeof(parameterSet) },
    };

    CK_MECHANISM keyGenMechanism = { CKM_ML_KEM_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;
    }

    // --- 캡슐화: 공개키로 공유 비밀(AES 키)을 생성하고 암호문을 얻는다 ---
    CK_MECHANISM kemMechanism = { CKM_ML_KEM, NULL_PTR, 0 };

    // 캡슐화로 만들어질 공유 비밀(AES-256) 키 객체의 속성
    CK_OBJECT_CLASS secretClass = CKO_SECRET_KEY;
    CK_KEY_TYPE aesKeyType = CKK_AES;
    CK_ULONG keyLen = 32;
    CK_ATTRIBUTE secretKeyTemplate[] = {
        { CKA_CLASS,      &secretClass, sizeof(secretClass) },
        { CKA_KEY_TYPE,   &aesKeyType,  sizeof(aesKeyType) },
        { CKA_VALUE_LEN,  &keyLen,      sizeof(keyLen) },
        { CKA_TOKEN,      &ckTrue,      sizeof(ckTrue) },
        { CKA_SENSITIVE,  &ckTrue,      sizeof(ckTrue) },
        { CKA_ENCRYPT,    &ckTrue,      sizeof(ckTrue) },
        { CKA_DECRYPT,    &ckTrue,      sizeof(ckTrue) },
    };
    CK_ULONG secretKeyTemplateCount = sizeof(secretKeyTemplate) / sizeof(CK_ATTRIBUTE);

    // ML-KEM 암호문은 파라미터 세트별로 크기가 고정되어 있으므로(FIPS 203, ML-KEM-768은 1088바이트),
    // C_Sign처럼 먼저 길이를 조회하는 대신 넉넉한 고정 버퍼를 미리 준비해 한 번에 호출한다.
    CK_ULONG ciphertextLen = 4096;
    CK_BYTE_PTR ciphertext = (CK_BYTE_PTR)malloc(ciphertextLen);
    CK_OBJECT_HANDLE hSenderSecret;
    rv = CA_EncapsulateKey(hSession, &kemMechanism, hPublicKey,
                            secretKeyTemplate, secretKeyTemplateCount,
                            ciphertext, &ciphertextLen, &hSenderSecret);
    if (rv != CKR_OK) {
        fprintf(stderr, "CA_EncapsulateKey failed: 0x%lx\n", rv);
        free(ciphertext);
        return 1;
    }
    printf("Encapsulated (ciphertext %lu bytes, shared secret handle=%lu)\n",
           (unsigned long)ciphertextLen, (unsigned long)hSenderSecret);

    // --- 역캡슐화: 개인키와 암호문으로 동일한 공유 비밀을 복원한다 ---
    CK_OBJECT_HANDLE hReceiverSecret;
    rv = CA_DecapsulateKey(hSession, &kemMechanism, hPrivateKey,
                            secretKeyTemplate, secretKeyTemplateCount,
                            ciphertext, ciphertextLen, &hReceiverSecret);
    if (rv != CKR_OK) {
        fprintf(stderr, "CA_DecapsulateKey failed: 0x%lx\n", rv);
        free(ciphertext);
        return 1;
    }
    printf("Decapsulated (shared secret handle=%lu)\n", (unsigned long)hReceiverSecret);

    // --- 공유 비밀 일치 여부 검증: 한쪽 키로 암호화하고 다른 쪽 키로 복호화해서 원문이 복원되는지 확인 ---
    CK_BYTE iv[16];
    p11->C_GenerateRandom(hSession, iv, sizeof(iv));
    CK_MECHANISM aesMechanism = { CKM_AES_CBC_PAD, iv, sizeof(iv) };

    CK_BYTE plaintext[] = "PQC KEM test data";
    CK_ULONG plaintextLen = sizeof(plaintext) - 1;

    // 송신측 공유 비밀(hSenderSecret)로 암호화
    p11->C_EncryptInit(hSession, &aesMechanism, hSenderSecret);
    CK_ULONG encryptedLen = 0;
    p11->C_Encrypt(hSession, plaintext, plaintextLen, NULL_PTR, &encryptedLen);
    CK_BYTE_PTR encrypted = (CK_BYTE_PTR)malloc(encryptedLen);
    p11->C_Encrypt(hSession, plaintext, plaintextLen, encrypted, &encryptedLen);

    // 수신측 공유 비밀(hReceiverSecret)로 복호화
    p11->C_DecryptInit(hSession, &aesMechanism, hReceiverSecret);
    CK_ULONG decryptedLen = 0;
    p11->C_Decrypt(hSession, encrypted, encryptedLen, NULL_PTR, &decryptedLen);
    CK_BYTE_PTR decrypted = (CK_BYTE_PTR)malloc(decryptedLen);
    p11->C_Decrypt(hSession, encrypted, encryptedLen, decrypted, &decryptedLen);

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

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

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

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

다음과 같은 결과가 출력되면 정상입니다(객체 핸들 값은 세션마다 다를 수 있습니다).

Encapsulated (ciphertext 1088 bytes, shared secret handle=6)
Decapsulated (shared secret handle=7)
Shared secret matches: true

ML-DSA 키 생성, 서명 및 검증

ML-DSA 키 쌍은 CKM_ML_DSA_KEY_PAIR_GEN mechanism으로 생성하고, CKA_PARAMETER_SET attribute로 파라미터 세트(44/65/87)를 지정합니다. 서명·검증은 CKM_ML_DSA mechanism을 사용합니다. mldsa_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;
    }

    // --- ML-DSA 키 쌍 생성 ---
    CK_ULONG parameterSet = CKP_ML_DSA_65;  // ML-DSA-65 (44/65/87 중 선택)
    CK_BBOOL ckTrue = CK_TRUE;

    CK_ATTRIBUTE publicKeyTemplate[] = {
        { CKA_TOKEN,          &ckTrue,       sizeof(ckTrue) },
        { CKA_PRIVATE,        &ckTrue,       sizeof(ckTrue) },
        { CKA_VERIFY,         &ckTrue,       sizeof(ckTrue) },
        { CKA_PARAMETER_SET,  &parameterSet, sizeof(parameterSet) },
    };
    CK_ATTRIBUTE privateKeyTemplate[] = {
        { CKA_TOKEN,          &ckTrue,       sizeof(ckTrue) },
        { CKA_PRIVATE,        &ckTrue,       sizeof(ckTrue) },
        { CKA_SENSITIVE,      &ckTrue,       sizeof(ckTrue) },
        { CKA_SIGN,           &ckTrue,       sizeof(ckTrue) },
        { CKA_PARAMETER_SET,  &parameterSet, sizeof(parameterSet) },
    };

    CK_MECHANISM keyGenMechanism = { CKM_ML_DSA_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_ML_DSA, NULL_PTR, 0 };
    CK_BYTE message[] = "PQC ML-DSA 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_ML_DSA, 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;
}

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

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

다음과 같은 결과가 출력되면 정상입니다. 서명 길이는 파라미터 세트별로 고정되어 있으며(ML-DSA-65는 FIPS 204 기준 3309바이트), 이 값이 그대로 출력됩니다.

Signature generated (3309 bytes)
Signature verification succeeded

HSS 키 생성

HSS 키 쌍은 CKM_HSS_KEY_PAIR_GEN mechanism으로 생성합니다. hss_keygen_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}";

    // Cryptoki 라이브러리 로드 및 함수 테이블 획득
    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;
    }

    // HSS 파라미터: 레벨 수 1, LMS_SHA256_M32_H10 / LMOTS_SHA256_N32_W4 (IANA 등록 표준값)
    CK_ULONG hssLevels = 1;
    CK_ULONG lmsType   = 0x00000006UL;  // LMS_SHA256_M32_H10
    CK_ULONG lmotsType = 0x00000003UL;  // LMOTS_SHA256_N32_W4
    CK_BBOOL ckTrue = CK_TRUE;

    // 공개키 템플릿에는 HSS 관련 속성을 넣지 않는다
    CK_ATTRIBUTE publicKeyTemplate[] = {
        { CKA_TOKEN,   &ckTrue, sizeof(ckTrue) },
        { CKA_PRIVATE, &ckTrue, sizeof(ckTrue) },
        { CKA_VERIFY,  &ckTrue, sizeof(ckTrue) },
    };

    // HSS 관련 속성(복수형: LMS_TYPES/LMOTS_TYPES)은 개인키 템플릿에만 지정한다
    CK_ATTRIBUTE privateKeyTemplate[] = {
        { CKA_TOKEN,           &ckTrue,    sizeof(ckTrue) },
        { CKA_PRIVATE,         &ckTrue,    sizeof(ckTrue) },
        { CKA_SENSITIVE,       &ckTrue,    sizeof(ckTrue) },
        { CKA_SIGN,            &ckTrue,    sizeof(ckTrue) },
        { CKA_HSS_LEVELS,      &hssLevels, sizeof(hssLevels) },
        { CKA_HSS_LMS_TYPES,   &lmsType,   sizeof(lmsType) },
        { CKA_HSS_LMOTS_TYPES, &lmotsType, sizeof(lmotsType) },
    };

    CK_MECHANISM keyGenMechanism = { CKM_HSS_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;
    }
    printf("HSS key pair generated (public=%lu, private=%lu)\n", (unsigned long)hPublicKey, (unsigned long)hPrivateKey);

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

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

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

다음과 같은 결과가 출력되면 정상입니다(객체 핸들 값은 세션마다 다를 수 있습니다).

HSS key pair generated (public=3, private=4)

HSS 서명 생성 및 검증

서명·검증은 CKM_HSS mechanism을 사용합니다. hss_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;
    }

    // --- HSS 키 쌍 생성 (자세한 설명은 "HSS 키 생성" 절 참고) ---
    CK_ULONG hssLevels = 1;
    CK_ULONG lmsType   = 0x00000006UL;  // LMS_SHA256_M32_H10
    CK_ULONG lmotsType = 0x00000003UL;  // LMOTS_SHA256_N32_W4
    CK_BBOOL ckTrue = CK_TRUE;

    CK_ATTRIBUTE publicKeyTemplate[] = {
        { CKA_TOKEN,   &ckTrue, sizeof(ckTrue) },
        { CKA_PRIVATE, &ckTrue, sizeof(ckTrue) },
        { CKA_VERIFY,  &ckTrue, sizeof(ckTrue) },
    };
    CK_ATTRIBUTE privateKeyTemplate[] = {
        { CKA_TOKEN,           &ckTrue,    sizeof(ckTrue) },
        { CKA_PRIVATE,         &ckTrue,    sizeof(ckTrue) },
        { CKA_SENSITIVE,       &ckTrue,    sizeof(ckTrue) },
        { CKA_SIGN,            &ckTrue,    sizeof(ckTrue) },
        { CKA_HSS_LEVELS,      &hssLevels, sizeof(hssLevels) },
        { CKA_HSS_LMS_TYPES,   &lmsType,   sizeof(lmsType) },
        { CKA_HSS_LMOTS_TYPES, &lmotsType, sizeof(lmotsType) },
    };

    CK_MECHANISM keyGenMechanism = { CKM_HSS_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_HSS, NULL_PTR, 0 };
    CK_BYTE message[] = "PQC HSS 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;
    }

    // 먼저 길이 0으로 호출해서 필요한 서명 버퍼 크기를 조회
    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) {
        if (rv == CKR_KEY_EXHAUSTED) {
            // HSS는 상태 유지형(stateful) 서명 방식이라 키마다 서명 가능 횟수가 정해져 있음
            // (자세한 내용은 "상태 유지 제약" 절 참고)
            fprintf(stderr, "C_Sign failed: CKR_KEY_EXHAUSTED - HSS one-time key exhausted\n");
        } else {
            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_HSS, 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;
}

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

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

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

Signature generated (2512 bytes)
Signature verification succeeded

상태 유지(state) 제약 — 반드시 확인하십시오

주의

HSS/LMS는 상태를 유지하는(stateful) 서명 방식입니다. 개인키 자체가 "남은 서명 가능 횟수"를 내장하고 매 서명마다 소진되며, 이 때문에 아래와 같은 근본적인 제약이 있습니다.

  • 개인키 복제(clone)가 불가능합니다. 그 결과로 백업/복원도 불가능하고, HA(고가용성) 그룹에도 포함시킬 수 없습니다. Thales 공식 문서: "HSS private keys cannot be cloned (therefore, no backup/restore, no inclusion in HA groups)." / "Do not add a partition to an HA group if the partition has an HSS private key on it."
  • 서명 가능 횟수가 소진되면 CKR_KEY_EXHAUSTED가 반환됩니다. CKA_HSS_KEYS_REMAINING attribute로 남은 서명 가능 횟수를 조회할 수 있습니다.
  • 백업 스크립트나 HA 파티션 구성에 HSS 개인키를 포함시키지 마십시오. NIST가 승인한 설계상의 제약이라 우회할 방법이 없습니다.

HA가 필요한 워크로드라면 HSS/LMS 대신 ML-DSA(격자 기반, stateless) 사용을 검토하십시오.

문제 해결

C_Login failed: 0x... (CKR_PIN_INCORRECT 등)

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

dlopen(...) failed 또는 컴파일 시 헤더를 찾을 수 없는 경우

LunaClient가 설치되어 있지 않거나, 빌드 방법의 경로가 실제 설치 경로와 다른 경우 발생합니다. LunaClient 10.9.3 설치 여부를 확인하십시오.

세션은 열리는데 로그인 이후 응답이 없거나 연결 오류가 나는 경우

HSM 클라이언트 서버와 HSM 파티션 간의 네트워크 연결(Connection)이나 파티션 초기 설정이 완료되지 않은 경우 발생할 수 있습니다. 전제 조건의 HSM 연결 생성·파티션 초기 설정이 완료됐는지 확인하십시오.