Hardware Security Module(HSM) 서비스는 Thales Luna HSM을 기반으로 하며, NIST 표준 양자내성암호(PQC) 알고리즘인 ML-KEM (FIPS 203, 키 캡슐화)과 ML-DSA (FIPS 204, 전자서명)를 지원합니다. Java Security Provider(LunaProvider)가 이를 표준 JCE(Java Cryptography Extension) API로 노출하므로, 별도의 네이티브 연동 코드 없이 표준 Java API로 사용할 수 있습니다.
이 문서에서는 Java 애플리케이션에서 LunaProvider를 통해 PQC 키를 생성하고, 키 캡슐화(KEM) 및 전자서명을 수행하는 방법을 안내합니다.
이 문서는 ML-KEM/ML-DSA만 다룹니다. HSS/LMS는 C++ 예제 가이드를, 전체 PQC 기능 목록은 양자내성암호(PQC) 사용 - 개요를 참고해 주십시오.
지원 환경
지원 환경을 설명합니다.
전제 조건
연동을 시작하기 전, 다음의 전제 조건을 확인해 주십시오.
- HSM 클라이언트 서버에 Java가 설치되어 있음
- HSM 클라이언트 서버에 Luna HSM Client(LunaClient) 10.9.3이 설치되어 있음(LunaClient 설치 가이드 참조)
- HSM 클라이언트 서버 인스턴스와 HSM 파티션 간에 연결 생성 완료
- HSM 파티션 초기 설정 완료
지원 버전
| 구성 요소 | 버전 | 비고 |
|---|---|---|
| HSM 클라이언트(Luna Client) | 10.9.3 | LunaClient 설치 가이드 |
| JDK | OpenJDK 또는 Oracle JDK 24 LTS / 25 LTS 이상 | Luna HSM Client 10.9.1 Customer Release Notes — "Java Lunaprovider Support for ML-KEM and ML-DSA" 섹션에 명시된 지원 버전 |
Luna HSM Java 설정
Java 애플리케이션에서 LunaProvider를 사용하려면 java.security 설정 파일에 LunaProvider를 등록해야 합니다.
security.provider.1=SUN
security.provider.2=com.safenetinc.luna.provider.LunaProvider
security.provider.3=SunRsaSign
security.provider.4=SunEC
security.provider.5=SunJSSE
security.provider.6=SunJCE
security.provider.7=SunJGSS
security.provider.8=SunSASL
security.provider.9=XMLDSig
security.provider.10=SunPCSC
security.provider.11=JdkLDAP
security.provider.12=JdkSASL
security.provider.13=SunPKCS11
또는 애플리케이션 코드에서 직접 Provider를 추가해도 됩니다. 아래 예제들은 모두 이 방식을 사용합니다.
Provider lunaProvider = new LunaProvider();
Security.addProvider(lunaProvider);
파티션 로그인
Provider를 추가한 것만으로는 HSM 파티션에 로그인되지 않습니다. 아래처럼 KeyStore를 로드하는 것이 곧 로그인 절차이며, 아래 모든 예제 코드에 포함되어 있습니다.
KeyStore luna = KeyStore.getInstance("Luna");
luna.load(new ByteArrayInputStream(("slot:" + SLOT).getBytes()),
PARTITION_PASSWORD.toCharArray());
슬롯 번호는 lunacm:> slot list로 확인할 수 있습니다.
컴파일 및 실행
# 1. 컴파일 — LunaProvider.jar를 클래스패스에 추가
javac -cp /usr/safenet/lunaclient/jsp/lib/LunaProvider.jar PqcKeyGenExample.java
# 2. 실행 — 컴파일된 클래스(.)와 LunaProvider.jar를 classpath에 넣고,
# -Djava.library.path는 java 옵션으로 직접 지정
java -cp .:/usr/safenet/lunaclient/jsp/lib/LunaProvider.jar \
-Djava.library.path=/usr/safenet/lunaclient/jsp/lib/ \
PqcKeyGenExample
다른 예제도 클래스 이름만 바꾸면 동일합니다.
PQC 키 생성
LunaProvider는 ML-KEM/ML-DSA를 각 파라미터 세트별 알고리즘 이름으로 등록합니다.
| 알고리즘 | 등록된 이름(algorithm name) |
|---|---|
| ML-KEM (키 캡슐화) | ML-KEM, ML-KEM-512, ML-KEM-768, ML-KEM-1024 |
| ML-DSA (전자서명) | ML-DSA, ML-DSA-44, ML-DSA-65, ML-DSA-87 |
'PqcKeyGenExample.java' 파일을 아래 내용으로 생성해 주십시오.
import java.io.ByteArrayInputStream;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.Provider;
import java.security.Security;
import com.safenetinc.luna.provider.LunaProvider;
public class PqcKeyGenExample {
// 실제 파티션의 슬롯 번호와 비밀번호로 교체하십시오.
private static final int SLOT = 0;
private static final String PARTITION_PASSWORD = "{partition-password}";
public static void main(String[] args) throws Exception {
Provider lunaProvider = new LunaProvider();
Security.addProvider(lunaProvider);
// 파티션 로그인 (KeyStore 로드 = 로그인)
KeyStore luna = KeyStore.getInstance("Luna");
luna.load(new ByteArrayInputStream(("slot:" + SLOT).getBytes()),
PARTITION_PASSWORD.toCharArray());
// ML-KEM-768 키 쌍 생성 (HSM 파티션 내부에서 생성되며 개인키는 반출되지 않음)
KeyPairGenerator kemKeyGen = KeyPairGenerator.getInstance("ML-KEM-768", lunaProvider);
KeyPair kemKeyPair = kemKeyGen.generateKeyPair();
// ML-DSA-65 키 쌍 생성
KeyPairGenerator dsaKeyGen = KeyPairGenerator.getInstance("ML-DSA-65", lunaProvider);
KeyPair dsaKeyPair = dsaKeyGen.generateKeyPair();
System.out.println("ML-KEM public key algorithm: " + kemKeyPair.getPublic().getAlgorithm());
System.out.println("ML-DSA public key algorithm: " + dsaKeyPair.getPublic().getAlgorithm());
}
}
컴파일 및 실행 후 다음과 같은 결과가 출력되면 정상입니다.
ML-KEM public key algorithm: ML-KEM-768
ML-DSA public key algorithm: ML-DSA-65
ML-KEM으로 키 캡슐화(KEM) 수행
ML-KEM은 javax.crypto.KEM API로 사용합니다(java.security.KEM이 아닙니다). PqcKemExample.java 파일을 아래 내용으로 생성해 주십시오.
import java.io.ByteArrayInputStream;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.Provider;
import java.security.Security;
import java.util.Arrays;
import javax.crypto.Cipher;
import javax.crypto.KEM;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import com.safenetinc.luna.provider.LunaProvider;
public class PqcKemExample {
// 실제 파티션의 슬롯 번호와 비밀번호로 교체하십시오.
private static final int SLOT = 0;
private static final String PARTITION_PASSWORD = "{partition-password}";
public static void main(String[] args) throws Exception {
Provider lunaProvider = new LunaProvider();
Security.addProvider(lunaProvider);
// 파티션 로그인
KeyStore luna = KeyStore.getInstance("Luna");
luna.load(new ByteArrayInputStream(("slot:" + SLOT).getBytes()),
PARTITION_PASSWORD.toCharArray());
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("ML-KEM-768", lunaProvider);
KeyPair keyPair = keyGen.generateKeyPair();
KEM kem = KEM.getInstance("ML-KEM", lunaProvider);
// 송신측: 공개키로 공유 비밀을 생성하고 캡슐화(encapsulate)
// encapsulate(from, to, algorithm)로 공유 비밀의 바이트 범위와 SecretKey 알고리즘을 지정
KEM.Encapsulator encapsulator = kem.newEncapsulator(keyPair.getPublic());
KEM.Encapsulated encapsulated = encapsulator.encapsulate(0, 32, "AES");
SecretKey senderSecret = encapsulated.key();
byte[] encapsulation = encapsulated.encapsulation();
// 수신측: 개인키와 캡슐(encapsulation)로 동일한 공유 비밀을 복원(decapsulate)
KEM.Decapsulator decapsulator = kem.newDecapsulator(keyPair.getPrivate());
SecretKey receiverSecret = decapsulator.decapsulate(encapsulation, 0, 32, "AES");
// HSM에 보관된 SecretKey는 non-extractable이라 getEncoded()로 직접 비교할 수 없으므로,
// 한쪽 키로 암호화하고 다른 쪽 키로 복호화해서 원문이 복원되는지로 공유 비밀 일치 여부를 검증
byte[] plaintext = "PQC KEM test data".getBytes("UTF-8");
Cipher encryptCipher = Cipher.getInstance("AES/CBC/PKCS5Padding", lunaProvider);
encryptCipher.init(Cipher.ENCRYPT_MODE, senderSecret);
byte[] ciphertext = encryptCipher.doFinal(plaintext);
IvParameterSpec iv = new IvParameterSpec(encryptCipher.getIV());
Cipher decryptCipher = Cipher.getInstance("AES/CBC/PKCS5Padding", lunaProvider);
decryptCipher.init(Cipher.DECRYPT_MODE, receiverSecret, iv);
byte[] decrypted = decryptCipher.doFinal(ciphertext);
System.out.println("Shared secret matches: " + Arrays.equals(plaintext, decrypted));
}
}
컴파일 및 실행 후 다음과 같은 결과가 출력되면 정상입니다.
Shared secret matches: true
ML-DSA로 전자서명 생성 및 검증
PqcSignatureExample.java 파일을 아래 내용으로 생성하십시오.
import java.io.ByteArrayInputStream;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.Provider;
import java.security.Security;
import java.security.Signature;
import com.safenetinc.luna.provider.LunaProvider;
public class PqcSignatureExample {
// 실제 파티션의 슬롯 번호와 비밀번호로 교체하십시오.
private static final int SLOT = 0;
private static final String PARTITION_PASSWORD = "{partition-password}";
public static void main(String[] args) throws Exception {
Provider lunaProvider = new LunaProvider();
Security.addProvider(lunaProvider);
// 파티션 로그인
KeyStore luna = KeyStore.getInstance("Luna");
luna.load(new ByteArrayInputStream(("slot:" + SLOT).getBytes()),
PARTITION_PASSWORD.toCharArray());
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("ML-DSA-65", lunaProvider);
KeyPair keyPair = keyGen.generateKeyPair();
byte[] message = "PQC signature test".getBytes("UTF-8");
// 서명
Signature signer = Signature.getInstance("ML-DSA", lunaProvider);
signer.initSign(keyPair.getPrivate());
signer.update(message);
byte[] signature = signer.sign();
// 검증
Signature verifier = Signature.getInstance("ML-DSA", lunaProvider);
verifier.initVerify(keyPair.getPublic());
verifier.update(message);
boolean verified = verifier.verify(signature);
System.out.println("Signature verification result: " + verified);
}
}
컴파일 및 실행 후 다음과 같은 결과가 출력되면 정상입니다.
Signature verification result: true
문제 해결
com.safenetinc.luna.exception.LunaException: No logged in tokens available
SLOT/PARTITION_PASSWORD를 실제 값으로 바꾸지 않았거나, 슬롯 번호나 비밀번호가 틀린 경우 발생합니다. lunacm:> slot list로 슬롯 번호를, 파티션 비밀번호를 다시 확인하십시오.
ClassNotFoundException: com.safenetinc.luna.provider.LunaProvider 또는 UnsatisfiedLinkError
LunaClient가 설치되어 있지 않거나, javac/java 명령의 -cp/-Djava.library.path 경로가 실제 설치 경로(/usr/safenet/lunaclient/jsp/lib/)와 다른 경우 발생합니다. LunaClient 설치 여부와 경로를 확인하십시오.
파티션 로그인은 되는데 키 생성/서명 호출에서 응답이 없거나 연결 오류가 나는 경우
HSM 클라이언트 서버와 HSM 파티션 간의 네트워크 연결(Connection)이나 파티션 초기 설정이 완료되지 않은 경우 발생할 수 있습니다. 전제 조건의 HSM 연결 생성·파티션 초기 설정이 완료됐는지 확인하십시오.