--- title: "C++ 예제" slug: "hsm-vpc-pqc-cpp" updated: 2026-09-17T09:09:36Z published: 2026-09-17T09:09:36Z canonical: "guide-fin.ncloud-docs.com/hsm-vpc-pqc-cpp" --- > ## 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++ 예제 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 예제 가이드](/docs/hsm-vpc-pqc-java)로도 사용할 수 있습니다. 전체 PQC 기능 목록은 [양자내성암호(PQC) — 개요](/docs/hsm-vpc-pqc)를 참고해 주십시오. 이 문서의 mechanism/attribute 명세는 Thales 공식 문서를 기준으로 작성되었습니다. - [ML-KEM Programming Guide (Luna SDK Reference)](https://www.thalesdocs.com/gphsm/luna/7/docs/network/Content/sdk/extensions/pqc/ML-KEM_programming_guide.htm) - [ML-DSA Programming Guide (Luna SDK Reference)](https://www.thalesdocs.com/gphsm/luna/7/docs/network/Content/sdk/extensions/pqc/ML-DSA_programming_guide.htm) - [LMS and HSS (Luna SDK Reference)](https://www.thalesdocs.com/gphsm/luna/7/docs/network/Content/sdk/extensions/pqc/LMS_and_HSS.htm) ## 지원 환경 ### 전제 조건 연동을 시작하기 전, 다음의 전제 조건을 확인해 주십시오. - HSM 클라이언트 서버에 C/C++ 빌드 도구(`g++`)가 설치되어 있음 - HSM 클라이언트 서버에 Luna HSM Client(LunaClient) 10.9.3이 설치되어 있음 — [LunaClient 설치 가이드](/docs/hsm-client) - HSM 클라이언트 서버 인스턴스와 HSM 파티션 간에 연결 생성 완료 - HSM 파티션 초기 설정 완료 ### 지원 버전 | 구성 요소 | 버전 | 비고 | | --- | --- | --- | | HSM 클라이언트(Luna Client) | 10.9.3 | [LunaClient 설치 가이드](/docs/hsm-client) | ## 빌드 방법 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 #include #include #include // 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 #include #include #include // 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, ¶meterSet, 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, ¶meterSet, 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; } ``` [빌드 방법](/docs/hsm-vpc-pqc-cpp#%EB%B9%8C%EB%93%9C%EB%B0%A9%EB%B2%95)에 따라 `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 #include #include #include 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, ¶meterSet, 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, ¶meterSet, 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; } ``` [빌드 방법](/docs/hsm-vpc-pqc-cpp#%EB%B9%8C%EB%93%9C%EB%B0%A9%EB%B2%95)에 따라 `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 #include #include #include 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; } ``` [빌드 방법](/docs/hsm-vpc-pqc-cpp#%EB%B9%8C%EB%93%9C%EB%B0%A9%EB%B2%95)에 따라 `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 #include #include #include 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; } ``` [빌드 방법](/docs/hsm-vpc-pqc-cpp#%EB%B9%8C%EB%93%9C%EB%B0%A9%EB%B2%95)에 따라 `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가 설치되어 있지 않거나, [빌드 방법](/docs/hsm-vpc-pqc-cpp#%EB%B9%8C%EB%93%9C%EB%B0%A9%EB%B2%95)의 경로가 실제 설치 경로와 다른 경우 발생합니다. LunaClient 10.9.3 설치 여부를 확인하십시오. ### 세션은 열리는데 로그인 이후 응답이 없거나 연결 오류가 나는 경우 HSM 클라이언트 서버와 HSM 파티션 간의 네트워크 연결(Connection)이나 파티션 초기 설정이 완료되지 않은 경우 발생할 수 있습니다. [전제 조건](/docs/hsm-vpc-pqc-cpp#%EC%A0%84%EC%A0%9C%EC%A1%B0%EA%B1%B4)의 HSM 연결 생성·파티션 초기 설정이 완료됐는지 확인하십시오.