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.

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 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 연결 생성·파티션 초기 설정이 완료됐는지 확인하십시오.