양자내성암호(PQC) Java 예제

Prev Next

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 이상) 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로 확인할 수 있습니다.

컴파일 및 실행

javac/java로 컴파일/실행해 주십시오.

# 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

문제 해결

Java 예제를 실행할 때 발생할 수 있는 문제와 해결 방법을 안내합니다.

로그인 실패

com.safenetinc.luna.exception.LunaException: No logged in tokens available 가 발생합니다.

원인

SLOT/PARTITION_PASSWORD를 실제 값으로 바꾸지 않았거나, 슬롯 번호나 비밀번호가 틀린 경우 발생합니다.

해결 방법

lunacm:> slot list로 슬롯 번호를, 파티션 비밀번호를 다시 확인해 주십시오.

LunaProvider 로드 실패

ClassNotFoundException: com.safenetinc.luna.provider.LunaProvider 또는 UnsatisfiedLinkError가 발생합니다.

원인

LunaClient가 설치되어 있지 않거나, javac/java 명령의 -cp/-Djava.library.path 경로가 실제 설치 경로(/usr/safenet/lunaclient/jsp/lib/)와 다른 경우 발생합니다.

해결 방법

LunaClient 설치 여부와 경로를 확인해 주십시오.

연결 오류

파티션 로그인은 되는데 키 생성/서명 호출에서 응답이 없거나 연결 오류가 납니다.

원인

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

해결 방법

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