보유한 RSA 공개키, RSA/EC 개인키, AES-256 대칭키를 NCP Hardware Security Module(HSM) 파티션에 주입(BYOK)하고, 주입한 키로 암호화·복호화와 서명·검증을 수행하는 절차를 Java(LunaProvider) 예제를 설명합니다. 주입 원리와 지원 범위는 BYOK(키 주입) 개요에서 먼저 확인해 주십시오.
이 문서는 위에서부터 순서대로 실행하면 전 과정이 완료되도록 구성했습니다. HsmByok.java와 KwpWrap.java를 한 번 컴파일해 두고, 명령을 바꿔가며 실행하는 방식입니다. 특정 키 유형만 필요한 경우, 1~3을 수행한 뒤 해당 절로 넘어가도 됩니다.
- 예제 프로그램 준비: 프로그램 작성·컴파일, 공통 환경변수 설정
- 샘플 키 준비: 주입해 볼 RSA/EC 키쌍과 AES 키 생성 (보유한 키가 있으면 생략)
- 1단계: wrapping 키쌍 생성과 공개키 반출
- AES-256 대칭키 주입: 주입 후 암호화·복호화 왕복 확인
- RSA 키 주입: 개인키·공개키 주입 후 서명·검증, 암호화·복호화 왕복 확인
- EC 키 주입: 개인키·공개키 주입 후 서명·검증 왕복 확인
- 주입한 키 조회/정리
이 문서의 절차는 접근이 통제된 서버 한 대에서 실행하는 것을 전제로 합니다. 키를 보유한 쪽의 작업과 HSM 클라이언트 서버의 작업을 같은 서버, 같은 셸에서 순서대로 실행하며, TS 값과 컴파일한 클래스 파일을 그대로 이어서 사용합니다.
각 명령 블록 앞에는 운영 환경에서 그 작업이 어느 쪽에 속하는지를 실행 위치로 표시했습니다. 운영 환경에서 두 시스템을 분리한다면, "키 보유 측 작업"으로 표시된 단계(OpenSSL 감싸기, KwpWrap 실행)는 키를 보유한 시스템에서 수행하고 HSM 클라이언트 서버로는 감싼 결과물(.b64)과 공개키(.pub.pem)만 전달하십시오. 평문 키 파일은 HSM 클라이언트 서버에 두지 마십시오.
지원 환경
지원 환경을 설명합니다.
전제 조건
연동을 시작하기 전, 다음의 전제 조건을 준수해 주십시오.
- HSM 클라이언트 서버에 Java가 설치 및 구동되고 있음
- HSM 클라이언트 서버에 Luna HSM Client(LunaClient)가 설치되어 있음: LunaClient 설치 가이드
- HSM 클라이언트 서버 인스턴스와 HSM 파티션 간에 연결 생성 완료
- HSM 파티션 초기 설정 완료
- 파티션 정책 확인 완료: 파티션 정책 요구사항
지원 버전
HSM 클라이언트 서버에 Luna HSM Client(LunaClient)를 설치하십시오. 설치 방법은 LunaClient 설치 가이드를 참고하십시오.
Java는 JDK 17 이상이 필요합니다. 개인키를 감쌀 때 사용하는 AES/KWP/NoPadding 변환이 JDK 17부터 표준 제공되기 때문입니다. 이 문서의 예제는 OpenJDK 17에서 확인했습니다.
키를 보유한 쪽에서 wrapped blob을 만들 때 OpenSSL을 사용합니다.
예제 프로그램 준비
HsmByok.java 작성
HsmByok.java 파일을 아래 내용으로 생성하십시오. 파티션에 로그인해 키 생성, 주입, 조회, 연산, 삭제를 명령으로 구분해 수행합니다.
import com.safenetinc.luna.LunaAPI;
import com.safenetinc.luna.LunaSlotManager;
import com.safenetinc.luna.LunaTokenObject;
import com.safenetinc.luna.attributes.LunaPkcs11Attributes;
import com.safenetinc.luna.provider.LunaProvider;
import com.safenetinc.luna.provider.key.LunaKey;
import com.safenetinc.luna.provider.param.LunaPkcs11AttributesParameterSpec;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Security;
import java.security.Signature;
import java.security.spec.AlgorithmParameterSpec;
import java.security.spec.RSAKeyGenParameterSpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Arrays;
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.Map;
public final class HsmByok {
private static final String PROVIDER = "LunaProvider";
private static final String OAEP = "RSA/ECB/OAEPWithSHA256AndMGF1Padding";
private final int slot;
private HsmByok(int slot, String pin) {
Provider provider = new LunaProvider();
if (Security.getProvider(provider.getName()) == null) {
Security.addProvider(provider);
}
this.slot = slot;
LunaSlotManager slots = LunaSlotManager.getInstance();
slots.setDefaultSlot(slot);
// 파티션 로그인
if (!slots.login(slot, pin)) {
throw new IllegalStateException("Partition login failed: slot=" + slot);
}
}
public static void main(String[] args) throws Exception {
if (args.length == 0 || "help".equals(args[0])) {
usage();
return;
}
String command = args[0];
Map<String, String> opt = parseArgs(Arrays.copyOfRange(args, 1, args.length));
HsmByok app = new HsmByok(Integer.parseInt(required(opt, "--slot")), required(opt, "--pin"));
switch (command) {
case "show" -> app.show(opt);
case "gen-rsa-wrap" -> app.genRsaWrap(opt);
case "import-public" -> app.importPublic(opt);
case "unwrap-aes-rsa-oaep" -> app.unwrapAesRsaOaep(opt);
case "unwrap-key-aes-kwp" -> app.unwrapKeyAesKwp(opt);
case "aes-cbc-roundtrip" -> app.aesCbcRoundtrip(opt);
case "rsa-oaep-encrypt" -> app.rsaOaepEncrypt(opt);
case "rsa-oaep-decrypt" -> app.rsaOaepDecrypt(opt);
case "sign" -> app.sign(opt);
case "verify" -> app.verify(opt);
case "destroy" -> app.destroy(opt);
default -> {
System.err.println("Unknown command: " + command);
usage();
System.exit(2);
}
}
}
private static void usage() {
System.out.println("""
HsmByok - common options: --slot <slot-id> --pin <CU password>
show --label <label> | --handle <handle>
gen-rsa-wrap --label <base-label> [--bits 2048] --public-out <pem>
import-public --label <label> --key-type RSA|EC --public-in <pem>
unwrap-aes-rsa-oaep --unwrapping-label <rsa-private-label> --label <new-label> --in <b64>
[--encrypt true] [--decrypt true] [--unwrap false]
unwrap-key-aes-kwp --unwrapping-label <aes-kek-label> --label <new-label>
--key-type RSA|EC --in <b64> [--sign true] [--decrypt false]
aes-cbc-roundtrip --label <aes-label> [--text <plaintext>]
rsa-oaep-encrypt --public-in <pem> --text <plaintext> --out <b64> (RSA 전용)
rsa-oaep-decrypt --label <private-key-label> --in <b64>
sign --label <private-key-label> --key-type RSA|EC [--text <message>] --out <b64>
verify --public-in <pem> --key-type RSA|EC [--text <message>] --in <b64>
destroy --label <label>
""");
}
// ---------- 조회 ----------
private void show(Map<String, String> opt) {
LunaTokenObject object;
String target;
if (opt.containsKey("--handle")) {
int handle = Integer.parseInt(opt.get("--handle"));
target = "handle=" + handle;
object = LunaTokenObject.LocateObjectByHandle(handle, slot);
} else {
String label = required(opt, "--label");
target = "label=" + label;
object = LunaTokenObject.LocateObjectByAlias(label, slot);
}
if (object == null) {
System.out.println("found=false " + target);
if (!opt.containsKey("--handle")) {
System.out.println(" hint: 공개키는 label 로 조회되지 않습니다. --handle 로 조회하십시오.");
}
return;
}
System.out.printf("found=true %s persistent=%s%n", target, object.IsObjectPersistent());
long[] attributes = {
LunaAPI.CKA_TOKEN, LunaAPI.CKA_PRIVATE, LunaAPI.CKA_SENSITIVE, LunaAPI.CKA_EXTRACTABLE,
LunaAPI.CKA_ENCRYPT, LunaAPI.CKA_DECRYPT, LunaAPI.CKA_SIGN, LunaAPI.CKA_VERIFY,
LunaAPI.CKA_WRAP, LunaAPI.CKA_UNWRAP
};
for (long attribute : attributes) {
try {
System.out.printf(" %s=%s%n", attributeName(attribute), object.GetBooleanAttribute(attribute));
} catch (Exception e) {
System.out.printf(" %s=(n/a)%n", attributeName(attribute));
}
}
}
// ---------- 1단계: wrapping 키쌍 생성 ----------
private void genRsaWrap(Map<String, String> opt) throws Exception {
String baseLabel = required(opt, "--label");
int bits = Integer.parseInt(opt.getOrDefault("--bits", "2048"));
LunaPkcs11Attributes attrs = new LunaPkcs11Attributes();
// 공개키: wrap 용도 (파티션 밖으로 반출)
attrs.setBooleanAttribute(LunaPkcs11Attributes.PUBLIC, LunaAPI.CKA_TOKEN, true);
attrs.setBooleanAttribute(LunaPkcs11Attributes.PUBLIC, LunaAPI.CKA_PRIVATE, false);
attrs.setBooleanAttribute(LunaPkcs11Attributes.PUBLIC, LunaAPI.CKA_WRAP, true);
attrs.setBooleanAttribute(LunaPkcs11Attributes.PUBLIC, LunaAPI.CKA_ENCRYPT, true);
// 개인키: unwrap 용도 (파티션 밖으로 나가지 않음)
attrs.setBooleanAttribute(LunaPkcs11Attributes.PRIVATE, LunaAPI.CKA_TOKEN, true);
attrs.setBooleanAttribute(LunaPkcs11Attributes.PRIVATE, LunaAPI.CKA_PRIVATE, true);
attrs.setBooleanAttribute(LunaPkcs11Attributes.PRIVATE, LunaAPI.CKA_SENSITIVE, true);
attrs.setBooleanAttribute(LunaPkcs11Attributes.PRIVATE, LunaAPI.CKA_EXTRACTABLE, false);
attrs.setBooleanAttribute(LunaPkcs11Attributes.PRIVATE, LunaAPI.CKA_UNWRAP, true);
attrs.setBooleanAttribute(LunaPkcs11Attributes.PRIVATE, LunaAPI.CKA_DECRYPT, true);
AlgorithmParameterSpec params = new LunaPkcs11AttributesParameterSpec(
attrs, new RSAKeyGenParameterSpec(bits, BigInteger.valueOf(65_537)));
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA", PROVIDER);
generator.initialize(params);
KeyPair pair = generator.generateKeyPair();
LunaKey publicKey = (LunaKey) pair.getPublic();
LunaKey privateKey = (LunaKey) pair.getPrivate();
publicKey.MakePersistent(baseLabel + "-pub");
privateKey.MakePersistent(baseLabel + "-prv");
Path out = Path.of(required(opt, "--public-out"));
writePem(out, "PUBLIC KEY", publicKey.getEncoded());
System.out.printf("Generated wrapping key pair: %s-pub / %s-prv%n", baseLabel, baseLabel);
System.out.println("Public key PEM: " + out);
}
// ---------- 공개키 주입 ----------
private void importPublic(Map<String, String> opt) throws Exception {
String keyType = keyType(opt);
byte[] der = readPem(Path.of(required(opt, "--public-in")));
PublicKey publicKey = KeyFactory.getInstance(keyType, PROVIDER)
.generatePublic(new X509EncodedKeySpec(der));
String label = required(opt, "--label");
LunaTokenObject object = persist((LunaKey) publicKey, label);
// CKA_PRIVATE 는 조정하지 않습니다. 주입한 공개키 객체에서는 이 속성이
// CKR_ATTRIBUTE_TYPE_INVALID 로 거부되며, CU 로그인 상태에서 사용하므로
// private 객체로 남아도 사용에 지장이 없습니다.
setBool(object, LunaAPI.CKA_VERIFY, bool(opt, "--verify", true));
if (keyType.equals("RSA")) {
// 암호화·wrap 은 RSA 공개키에만 해당합니다.
setBool(object, LunaAPI.CKA_ENCRYPT, bool(opt, "--encrypt", true));
setBool(object, LunaAPI.CKA_WRAP, bool(opt, "--wrap", false));
}
System.out.printf("Imported %s public key label=%s handle=%d%n",
keyType, label, ((LunaKey) publicKey).GetKeyHandle());
}
// ---------- 대칭키 주입 (RSA-OAEP unwrap) ----------
private void unwrapAesRsaOaep(Map<String, String> opt) throws Exception {
LunaKey unwrappingKey = findKey(required(opt, "--unwrapping-label"));
byte[] wrapped = readBase64(Path.of(required(opt, "--in")));
Cipher cipher = Cipher.getInstance(OAEP, PROVIDER);
cipher.init(Cipher.UNWRAP_MODE, unwrappingKey);
SecretKey key = (SecretKey) cipher.unwrap(wrapped, "AES", Cipher.SECRET_KEY);
String label = required(opt, "--label");
LunaTokenObject object = persist((LunaKey) key, label);
setBool(object, LunaAPI.CKA_ENCRYPT, bool(opt, "--encrypt", true));
setBool(object, LunaAPI.CKA_DECRYPT, bool(opt, "--decrypt", true));
setBool(object, LunaAPI.CKA_UNWRAP, bool(opt, "--unwrap", false));
System.out.printf("Imported AES key label=%s%n", label);
}
// ---------- 개인키 주입 (AES-KWP unwrap) ----------
private void unwrapKeyAesKwp(Map<String, String> opt) throws Exception {
String keyType = keyType(opt);
LunaKey kek = findKey(required(opt, "--unwrapping-label"));
byte[] wrapped = readBase64(Path.of(required(opt, "--in")));
Cipher cipher = Cipher.getInstance("AES/KWP/NoPadding", PROVIDER);
cipher.init(Cipher.UNWRAP_MODE, kek);
PrivateKey key = (PrivateKey) cipher.unwrap(wrapped, keyType, Cipher.PRIVATE_KEY);
String label = required(opt, "--label");
LunaTokenObject object = persist((LunaKey) key, label);
setBool(object, LunaAPI.CKA_SIGN, bool(opt, "--sign", true));
setBool(object, LunaAPI.CKA_DECRYPT, bool(opt, "--decrypt", false));
System.out.printf("Imported %s private key label=%s%n", keyType, label);
}
// ---------- 연산 ----------
private void aesCbcRoundtrip(Map<String, String> opt) throws Exception {
LunaKey key = findKey(required(opt, "--label"));
byte[] plaintext = opt.getOrDefault("--text", "byok aes test").getBytes(StandardCharsets.UTF_8);
byte[] iv = new byte[16];
SecureRandom.getInstanceStrong().nextBytes(iv);
Cipher encrypt = Cipher.getInstance("AES/CBC/PKCS5Padding", PROVIDER);
encrypt.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
byte[] ciphertext = encrypt.doFinal(plaintext);
Cipher decrypt = Cipher.getInstance("AES/CBC/PKCS5Padding", PROVIDER);
decrypt.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
byte[] decrypted = decrypt.doFinal(ciphertext);
System.out.println("ciphertextBase64=" + Base64.getEncoder().encodeToString(ciphertext));
System.out.println("roundtripOk=" + Arrays.equals(plaintext, decrypted));
}
private void rsaOaepEncrypt(Map<String, String> opt) throws Exception {
PublicKey key = publicKey(opt);
byte[] plaintext = required(opt, "--text").getBytes(StandardCharsets.UTF_8);
Cipher cipher = Cipher.getInstance(OAEP, PROVIDER);
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] ciphertext = cipher.doFinal(plaintext);
Path out = Path.of(required(opt, "--out"));
writeBase64(out, ciphertext);
System.out.println("ciphertextBytes=" + ciphertext.length);
System.out.println("Output file: " + out);
}
private void rsaOaepDecrypt(Map<String, String> opt) throws Exception {
LunaKey key = findKey(required(opt, "--label"));
byte[] ciphertext = readBase64(Path.of(required(opt, "--in")));
Cipher cipher = Cipher.getInstance(OAEP, PROVIDER);
cipher.init(Cipher.DECRYPT_MODE, (PrivateKey) key);
byte[] plaintext = cipher.doFinal(ciphertext);
System.out.println("decryptedText=" + new String(plaintext, StandardCharsets.UTF_8));
}
private void sign(Map<String, String> opt) throws Exception {
LunaKey key = findKey(required(opt, "--label"));
byte[] message = opt.getOrDefault("--text", "byok signature test").getBytes(StandardCharsets.UTF_8);
Signature signer = Signature.getInstance(signatureAlgorithm(keyType(opt)), PROVIDER);
signer.initSign((PrivateKey) key);
signer.update(message);
byte[] signature = signer.sign();
System.out.println("signatureBytes=" + signature.length);
if (opt.containsKey("--out")) {
Path out = Path.of(opt.get("--out"));
writeBase64(out, signature);
System.out.println("Output file: " + out);
}
}
private void verify(Map<String, String> opt) throws Exception {
PublicKey key = publicKey(opt);
byte[] message = opt.getOrDefault("--text", "byok signature test").getBytes(StandardCharsets.UTF_8);
byte[] signature = readBase64(Path.of(required(opt, "--in")));
Signature verifier = Signature.getInstance(
signatureAlgorithm(keyType(opt)), PROVIDER);
verifier.initVerify(key);
verifier.update(message);
System.out.println("verified=" + verifier.verify(signature));
}
private void destroy(Map<String, String> opt) {
LunaKey key = findKey(required(opt, "--label"));
int handle = key.GetKeyHandle();
key.DestroyKey();
System.out.printf("Destroyed handle=%d%n", handle);
}
// ---------- 유틸 ----------
/**
* 공개키를 얻습니다. PEM 을 지정하면 LunaProvider 세션 객체로 올려서 사용하므로
* 연산 자체는 HSM 안에서 수행됩니다. 주입한 공개키는 label 로 조회되지 않으므로
* PEM 지정을 기본 경로로 삼으십시오.
*/
private PublicKey publicKey(Map<String, String> opt) throws Exception {
if (opt.containsKey("--public-in")) {
byte[] der = readPem(Path.of(opt.get("--public-in")));
return (PublicKey) KeyFactory.getInstance(keyType(opt), PROVIDER)
.generatePublic(new X509EncodedKeySpec(der));
}
return (PublicKey) findKey(required(opt, "--label"));
}
private LunaKey findKey(String label) {
LunaKey key = LunaKey.LocateKeyByAlias(label, slot);
if (key == null) {
throw new IllegalStateException("Key not found: label=" + label + " slot=" + slot);
}
return key;
}
/**
* 토큰(영구) 객체로 기록하고 그 객체를 반환합니다.
* label 조회는 공개키를 찾지 못하므로, MakePersistent 가 갱신한 핸들로 조회합니다.
*/
private LunaTokenObject persist(LunaKey key, String label) {
key.MakePersistent(label);
int handle = key.GetKeyHandle();
LunaTokenObject object = LunaTokenObject.LocateObjectByHandle(handle, slot);
if (object == null) {
throw new IllegalStateException(
"Key was not persisted as a token object: label=" + label + " slot=" + slot);
}
return object;
}
private void setBool(LunaTokenObject object, long attribute, boolean value) {
try {
object.SetBooleanAttribute(attribute, value);
} catch (Exception e) {
// 객체 생성 이후에는 읽기 전용이 되는 속성이 있습니다.
// 의도한 연산이 CKR_KEY_FUNCTION_NOT_PERMITTED 로 실패하면 이 경고를 먼저 확인하십시오.
System.err.printf("[warn] failed to set attribute=%s value=%s (%s)%n",
attributeName(attribute), value, e.getMessage());
}
}
private static String attributeName(long attribute) {
if (attribute == LunaAPI.CKA_TOKEN) return "CKA_TOKEN";
if (attribute == LunaAPI.CKA_PRIVATE) return "CKA_PRIVATE";
if (attribute == LunaAPI.CKA_SENSITIVE) return "CKA_SENSITIVE";
if (attribute == LunaAPI.CKA_EXTRACTABLE) return "CKA_EXTRACTABLE";
if (attribute == LunaAPI.CKA_ENCRYPT) return "CKA_ENCRYPT";
if (attribute == LunaAPI.CKA_DECRYPT) return "CKA_DECRYPT";
if (attribute == LunaAPI.CKA_SIGN) return "CKA_SIGN";
if (attribute == LunaAPI.CKA_VERIFY) return "CKA_VERIFY";
if (attribute == LunaAPI.CKA_WRAP) return "CKA_WRAP";
if (attribute == LunaAPI.CKA_UNWRAP) return "CKA_UNWRAP";
return "0x" + Long.toHexString(attribute);
}
/** --key-type 을 RSA 또는 EC 로 정규화합니다. 지정하지 않으면 RSA 입니다. */
private static String keyType(Map<String, String> opt) {
String value = opt.getOrDefault("--key-type", "RSA").toUpperCase();
if (value.equals("ECDSA")) {
value = "EC";
}
if (!value.equals("RSA") && !value.equals("EC")) {
throw new IllegalArgumentException("--key-type must be RSA or EC");
}
return value;
}
private static String signatureAlgorithm(String keyType) {
return switch (keyType.toUpperCase()) {
case "RSA" -> "SHA256withRSA";
case "EC", "ECDSA" -> "SHA256withECDSA";
default -> throw new IllegalArgumentException("Unsupported key type: " + keyType);
};
}
private static boolean bool(Map<String, String> opt, String name, boolean defaultValue) {
return opt.containsKey(name) ? Boolean.parseBoolean(opt.get(name)) : defaultValue;
}
private static Map<String, String> parseArgs(String[] args) {
Map<String, String> opt = new LinkedHashMap<>();
for (int i = 0; i < args.length; i++) {
if (!args[i].startsWith("--")) {
throw new IllegalArgumentException("Expected option name, got: " + args[i]);
}
if (i + 1 >= args.length || args[i + 1].startsWith("--")) {
opt.put(args[i], "true");
} else {
opt.put(args[i], args[++i]);
}
}
return opt;
}
private static String required(Map<String, String> opt, String key) {
String value = opt.get(key);
if (value == null || value.isBlank()) {
throw new IllegalArgumentException("Missing required option: " + key);
}
return value;
}
private static void writePem(Path out, String type, byte[] der) throws IOException {
String body = Base64.getMimeEncoder(64, "\n".getBytes(StandardCharsets.US_ASCII)).encodeToString(der);
Files.writeString(out, "-----BEGIN " + type + "-----\n" + body + "\n-----END " + type + "-----\n",
StandardCharsets.US_ASCII);
}
private static byte[] readPem(Path in) throws IOException {
String text = Files.readString(in, StandardCharsets.US_ASCII)
.replaceAll("-----(BEGIN|END)[^-]*-----", "")
.replaceAll("\\s+", "");
return Base64.getDecoder().decode(text);
}
private static void writeBase64(Path out, byte[] value) throws IOException {
Files.writeString(out,
Base64.getMimeEncoder(64, "\n".getBytes(StandardCharsets.US_ASCII)).encodeToString(value) + "\n",
StandardCharsets.US_ASCII);
}
private static byte[] readBase64(Path in) throws IOException {
return Base64.getDecoder().decode(
Files.readString(in, StandardCharsets.US_ASCII).replaceAll("\\s+", ""));
}
}
LunaProvider에서 파티션에 로그인하는 방법은 KeyStore.load()와 LunaSlotManager.login() 두 가지가 있습니다. 이 예제는 후자를 사용하는데, 키 속성을 직접 다루는 데 필요한 LunaAPI 인스턴스를 함께 얻을 수 있기 때문입니다.
KwpWrap.java 작성
개인키를 주입할 때 개인키를 임시 AES 키로 감싸는 데 사용하는 헬퍼입니다. 이 예제에서는 HsmByok.java와 같은 서버에서 컴파일해 사용합니다. LunaProvider가 필요 없으므로, 운영 환경에서 두 시스템을 분리한다면 키를 보유한 시스템에서 컴파일·실행하면 됩니다. 공개키와 대칭키만 주입한다면 이 파일은 필요하지 않습니다.
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.Key;
import java.security.KeyFactory;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
public class KwpWrap {
public static void main(String[] args) throws Exception {
if (args.length != 4) {
throw new IllegalArgumentException("usage: KwpWrap RSA|EC <kek.bin> <pkcs8.der> <out.b64>");
}
byte[] kek = Files.readAllBytes(Path.of(args[1]));
byte[] pkcs8 = Files.readAllBytes(Path.of(args[2]));
Key key = KeyFactory.getInstance(args[0]).generatePrivate(new PKCS8EncodedKeySpec(pkcs8));
Cipher cipher = Cipher.getInstance("AES/KWP/NoPadding");
cipher.init(Cipher.WRAP_MODE, new SecretKeySpec(kek, "AES"));
byte[] wrapped = cipher.wrap(key);
Files.writeString(Path.of(args[3]),
Base64.getMimeEncoder(64, new byte[] {'\n'}).encodeToString(wrapped) + "\n");
System.out.println("wrappedBytes=" + wrapped.length);
}
}
컴파일
실행 위치 — HSM 클라이언트 서버
javac -encoding UTF-8 -cp /usr/safenet/lunaclient/jsp/lib/LunaProvider.jar HsmByok.java
javac -encoding UTF-8 KwpWrap.java
소스 파일을 UTF-8로 저장하고, javac에 -encoding UTF-8을 반드시 지정하십시오. 위 소스에는 한글 주석이 들어 있는데, 서버 로케일이 C/POSIX인 경우 javac의 기본 인코딩이 US-ASCII가 되어 error: unmappable character 로 컴파일이 실패합니다.
실행 공통값
실행 위치 — HSM 클라이언트 서버
이후의 모든 명령에서 사용할 값입니다. SLOT과 CU_PASS를 실제 파티션의 슬롯 번호와 Crypto User(CU) 비밀번호로 교체하십시오.
export SLOT=0
export CU_PASS='{partition-password}'
export TS=$(date +%Y%m%d%H%M%S)
export BYOK="java -cp $PWD:/usr/safenet/lunaclient/jsp/lib/LunaProvider.jar \
-Djava.library.path=/usr/safenet/lunaclient/jsp/lib HsmByok"
TS는 이 문서의 예제에서 label과 파일 이름이 겹치지 않도록 붙이는 타임스탬프입니다. 아래 절차를 이어서 진행하는 동안 값을 바꾸지 마십시오.
슬롯 번호는 lunacm:> slot list로 확인할 수 있습니다.
HA(고가용성) 그룹으로 구성된 경우 SLOT에는 개별 파티션 슬롯이 아니라 HA 가상 슬롯 ID를 지정해야 합니다. 개별 슬롯에 주입하면 그 파티션에만 키가 생기고 HA 멤버 간에 동기화되지 않습니다.
준비가 끝나면 명령 목록이 출력됩니다.
$BYOK help
샘플 키 준비
이 문서의 절차를 그대로 따라 해 볼 수 있도록, 주입할 샘플 키를 만듭니다. 실제로 주입할 키를 이미 보유하고 있다면 이 절을 건너뛰고 이후 명령의 파일 경로만 보유하신 키 파일로 바꿔 주십시오.
실행 위치 — 키 보유 측 작업
이 예제에서는 같은 서버에서 실행합니다.
# RSA-2048 키쌍 — 개인키, 공개키, 주입용 PKCS#8 DER
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out "/tmp/sample-rsa-$TS.pem"
openssl rsa -in "/tmp/sample-rsa-$TS.pem" -pubout -out "/tmp/sample-rsa-$TS.pub.pem"
openssl pkcs8 -topk8 -nocrypt \
-in "/tmp/sample-rsa-$TS.pem" -outform DER -out "/tmp/sample-rsa-$TS.pk8.der"
# EC P-256 키쌍 — 개인키, 공개키, 주입용 PKCS#8 DER
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out "/tmp/sample-ec-$TS.pem"
openssl ec -in "/tmp/sample-ec-$TS.pem" -pubout -out "/tmp/sample-ec-$TS.pub.pem"
openssl pkcs8 -topk8 -nocrypt \
-in "/tmp/sample-ec-$TS.pem" -outform DER -out "/tmp/sample-ec-$TS.pk8.der"
# AES-256 대칭키 (32바이트)
openssl rand -out "/tmp/sample-aes-$TS.bin" 32
ls -l /tmp/sample-*-$TS.*
| 파일 | 역할 |
|---|---|
sample-rsa-$TS.pem |
RSA 개인키 평문. 감싸는 데만 쓰고 전달하지 않음 |
sample-rsa-$TS.pub.pem |
RSA 공개키. 주입과 서명 검증·암호화에 사용 |
sample-rsa-$TS.pk8.der |
RSA 개인키 PKCS#8 DER. 감싸기 입력 |
sample-ec-$TS.pem |
EC 개인키 평문 |
sample-ec-$TS.pub.pem |
EC 공개키 |
sample-ec-$TS.pk8.der |
EC 개인키 PKCS#8 DER |
sample-aes-$TS.bin |
AES-256 키 평문 32바이트 |
.pem, .pk8.der, .bin 은 키 평문입니다. 이 예제는 한 서버에서 실행하므로 같은 서버에 생성되지만, 운영 환경에서 두 시스템을 분리한다면 이 파일들을 HSM 클라이언트 서버로 옮기지 마십시오. HSM 쪽으로 넘기는 것은 감싼 결과물(.b64)과 공개키(.pub.pem)뿐입니다. 모든 절차가 끝나면 정리에서 삭제하십시오.
1단계: wrapping 키쌍 생성과 공개키 반출
공개키만 주입하는 경우를 제외하면, 모든 주입 시나리오는 이 단계에서 시작합니다. 파티션 안에 RSA wrapping 키쌍을 만들고 공개키만 PEM으로 반출합니다.
실행 위치 — HSM 클라이언트 서버
$BYOK gen-rsa-wrap \
--slot "$SLOT" --pin "$CU_PASS" \
--label "byok-wrap-$TS" \
--bits 2048 \
--public-out "/tmp/byok-wrap-$TS.pem"
다음과 같은 결과가 출력되면 정상입니다.
Generated wrapping key pair: byok-wrap-20260806103000-pub / byok-wrap-20260806103000-prv
Public key PEM: /tmp/byok-wrap-20260806103000.pem
이 예제에서는 같은 서버에서 이어서 사용하므로 별도 전달이 필요 없습니다. 운영 환경에서 두 시스템을 분리한다면 생성된 /tmp/byok-wrap-$TS.pem 파일을 키 보유 측에 전달하십시오. 공개키이므로 별도의 보호 없이 전달해도 됩니다. 반면 -prv 개인키는 파티션 밖으로 나가지 않습니다.
AES-256 대칭키 주입
AES 대칭키는 HSM wrapping 공개키로 한 번 감싸서 주입합니다.
키 보유 측 AES 키 감싸기
실행 위치: 키 보유 측 작업 (이 예제에서는 같은 서버에서 실행)
openssl pkeyutl -encrypt \
-pubin -inkey "/tmp/byok-wrap-$TS.pem" \
-in "/tmp/sample-aes-$TS.bin" \
-out "/tmp/sample-aes-$TS.oaep.bin" \
-pkeyopt rsa_padding_mode:oaep \
-pkeyopt rsa_oaep_md:sha256 \
-pkeyopt rsa_mgf1_md:sha256
base64 "/tmp/sample-aes-$TS.oaep.bin" > "/tmp/sample-aes-$TS.oaep.b64"
rsa_oaep_md와 rsa_mgf1_md를 모두 sha256으로 지정해야 합니다. OpenSSL 기본값은 SHA-1이라, 생략하면 HSM에서 unwrap이 실패합니다.
운영 환경에서 두 시스템을 분리한다면 /tmp/sample-aes-$TS.oaep.b64 파일만 HSM 클라이언트 서버로 전달하고, /tmp/sample-aes-$TS.bin 평문 파일은 전달하지 마십시오.
파티션에 주입
실행 위치: HSM 클라이언트 서버
$BYOK unwrap-aes-rsa-oaep \
--slot "$SLOT" --pin "$CU_PASS" \
--unwrapping-label "byok-wrap-$TS-prv" \
--label "aes-$TS" \
--in "/tmp/sample-aes-$TS.oaep.b64" \
--encrypt true --decrypt true --unwrap false
Imported AES key label=aes-20260806103000
여러 개의 AES 키를 주입하려면 키마다 --label을 다르게 지정해 이 절차를 반복하십시오. wrapping 키쌍은 재사용할 수 있습니다.
암호화·복호화 왕복 테스트
실행 위치: HSM 클라이언트 서버
$BYOK aes-cbc-roundtrip \
--slot "$SLOT" --pin "$CU_PASS" \
--label "aes-$TS" \
--text "byok aes test"
ciphertextBase64=Fkdonc9EufPvB2sq5584Tw==
roundtripOk=true
roundtripOk=true는 HSM 안에서 암호화한 결과를 같은 키로 복호화해 원문이 그대로 복원됐다는 뜻입니다.
RSA 키 주입
RSA는 개인키와 공개키를 모두 주입한 뒤, 서명·검증과 암호화·복호화를 각각 왕복으로 확인합니다.
개인키는 크기 때문에 RSA-OAEP로 직접 감쌀 수 없습니다. 임시 AES KEK로 개인키를 감싸고, 그 KEK만 HSM 공개키로 감싸서 전달합니다. 파티션 안에서는 unwrap을 두 번 수행합니다.
키 보유 측: 임시 KEK 생성과 개인키 감싸기
실행 위치: 키 보유 측 작업 (이 예제에서는 같은 서버에서 실행)
# 1. 임시 AES-256 KEK 생성
openssl rand -out "/tmp/kek-$TS.bin" 32
# 2. KEK 를 HSM 공개키로 RSA-OAEP 감싸기
openssl pkeyutl -encrypt \
-pubin -inkey "/tmp/byok-wrap-$TS.pem" \
-in "/tmp/kek-$TS.bin" \
-out "/tmp/kek-$TS.oaep.bin" \
-pkeyopt rsa_padding_mode:oaep \
-pkeyopt rsa_oaep_md:sha256 \
-pkeyopt rsa_mgf1_md:sha256
base64 "/tmp/kek-$TS.oaep.bin" > "/tmp/kek-$TS.oaep.b64"
# 3. RSA 개인키를 KEK 로 AES-KWP 감싸기
java KwpWrap RSA \
"/tmp/kek-$TS.bin" \
"/tmp/sample-rsa-$TS.pk8.der" \
"/tmp/sample-rsa-$TS.kwp.b64"
wrappedBytes=1232
운영 환경에서 두 시스템을 분리한다면 HSM 클라이언트 서버로는 kek-$TS.oaep.b64, sample-rsa-$TS.kwp.b64, sample-rsa-$TS.pub.pem 세 파일만 전달합니다.
파티션에 임시 KEK 주입
KEK는 개인키를 풀기 위한 용도로만 쓰므로 --unwrap true만 켜고 암복호화 권한은 끕니다.
실행 위치: HSM 클라이언트 서버
$BYOK unwrap-aes-rsa-oaep \
--slot "$SLOT" --pin "$CU_PASS" \
--unwrapping-label "byok-wrap-$TS-prv" \
--label "kek-$TS" \
--in "/tmp/kek-$TS.oaep.b64" \
--encrypt false --decrypt false --unwrap true
Imported AES key label=kek-20260806103000
파티션에 RSA 개인키 주입
서명과 RSA-OAEP 복호화에 모두 사용하므로 --sign true --decrypt true를 지정합니다.
실행 위치: HSM 클라이언트 서버
$BYOK unwrap-key-aes-kwp \
--slot "$SLOT" --pin "$CU_PASS" \
--unwrapping-label "kek-$TS" \
--label "rsa-prv-$TS" \
--key-type RSA \
--in "/tmp/sample-rsa-$TS.kwp.b64" \
--sign true --decrypt true
Imported RSA private key label=rsa-prv-20260806103000
파티션에 RSA 공개키 주입
공개키는 공개 정보이므로 감쌀 필요 없이 PEM 파일을 그대로 주입합니다.
실행 위치 — HSM 클라이언트 서버
$BYOK import-public \
--slot "$SLOT" --pin "$CU_PASS" \
--key-type RSA \
--label "rsa-pub-$TS" \
--public-in "/tmp/sample-rsa-$TS.pub.pem"
Imported RSA public key label=rsa-pub-20260806103000 handle={handle}
출력된 handle을 기록해 두십시오. 공개키는 label로 조회되지 않으므로 확인할 때 handle이 필요합니다.
$BYOK show --slot "$SLOT" --pin "$CU_PASS" --handle {handle}
found=true handle={handle} persistent=true
CKA_TOKEN=true
CKA_PRIVATE=true
CKA_SENSITIVE=(n/a)
CKA_EXTRACTABLE=(n/a)
CKA_ENCRYPT=true
CKA_DECRYPT=(n/a)
CKA_SIGN=(n/a)
CKA_VERIFY=true
CKA_WRAP=false
CKA_UNWRAP=(n/a)
(n/a)는 공개키 객체에 존재하지 않는 속성이라는 뜻입니다.
주입한 공개키는 파티션에 정상 보관되지만, LunaProvider의 label 조회로는 찾을 수 없습니다. LunaProvider의 label 기반 탐색 API는 개인키·대칭키·인증서만 대상으로 하며 공개키 객체는 검색하지 않습니다. 파티션 안에서 생성한 공개키도 마찬가지입니다.
따라서 아래 검증·암호화 명령은 label 대신 공개키 PEM을 지정합니다. 이때 LunaProvider가 공개키를 세션 객체로 파티션에 올리므로 연산 자체는 HSM 안에서 수행되며, 보안상 차이는 없습니다. 공개키에는 보호할 비밀이 없으므로 영구 보관은 인벤토리 목적입니다.
주입할 공개키 PEM이 -----BEGIN PUBLIC KEY-----(SubjectPublicKeyInfo)로 시작하는지 확인하십시오. -----BEGIN RSA PUBLIC KEY-----(PKCS#1)로 시작한다면 그대로 사용할 수 없으므로, 그 경우에만 아래 명령으로 변환한 뒤 주입합니다. 위 샘플 키 준비에서 만든 PEM은 이미 SubjectPublicKeyInfo 형식이므로 변환이 필요 없습니다.
openssl rsa -RSAPublicKey_in -in pkcs1-pub.pem -pubout -out spki-pub.pem
서명·검증 왕복 테스트
주입한 개인키로 서명하고, 같은 키쌍의 공개키로 검증합니다.
실행 위치 — HSM 클라이언트 서버
$BYOK sign \
--slot "$SLOT" --pin "$CU_PASS" \
--label "rsa-prv-$TS" \
--key-type RSA \
--text "byok signature test" \
--out "/tmp/rsa-sig-$TS.b64"
$BYOK verify \
--slot "$SLOT" --pin "$CU_PASS" \
--key-type RSA \
--public-in "/tmp/sample-rsa-$TS.pub.pem" \
--text "byok signature test" \
--in "/tmp/rsa-sig-$TS.b64"
RSA-2048 키의 서명은 항상 256바이트입니다.
signatureBytes=256
Output file: /tmp/rsa-sig-20260806103000.b64
verified=true
verified=true는 주입한 개인키가 만든 서명이 원본 공개키로 검증됐다는 뜻이며, 개인키가 손상 없이 주입됐음을 확인해 줍니다.
암호화·복호화 왕복 테스트
공개키로 RSA-OAEP 암호화하고, 주입한 개인키로 복호화합니다.
실행 위치 — HSM 클라이언트 서버
$BYOK rsa-oaep-encrypt \
--slot "$SLOT" --pin "$CU_PASS" \
--public-in "/tmp/sample-rsa-$TS.pub.pem" \
--text "byok oaep test" \
--out "/tmp/oaep-ct-$TS.b64"
$BYOK rsa-oaep-decrypt \
--slot "$SLOT" --pin "$CU_PASS" \
--label "rsa-prv-$TS" \
--in "/tmp/oaep-ct-$TS.b64"
ciphertextBytes=256
Output file: /tmp/oaep-ct-20260806103000.b64
decryptedText=byok oaep test
EC 키 주입
EC도 RSA와 동일한 이중 wrap 방식으로 개인키를 주입하고, 공개키는 그대로 주입합니다. --key-type만 EC로 바꾸면 됩니다. EC 공개키로는 암호화를 수행하지 않으므로 서명·검증만 확인합니다.
RSA 키 주입에서 만든 임시 KEK(kek-$TS)를 그대로 재사용합니다. EC만 주입하시는 경우에는 키 보유 측 — 임시 KEK 생성과 개인키 감싸기의 1·2번과 파티션에 임시 KEK 주입을 먼저 수행하십시오.
키 보유 측 — 개인키 감싸기
실행 위치 — 키 보유 측 작업
이 예제에서는 같은 서버에서 실행합니다.
java KwpWrap EC \
"/tmp/kek-$TS.bin" \
"/tmp/sample-ec-$TS.pk8.der" \
"/tmp/sample-ec-$TS.kwp.b64"
wrappedBytes=152
파티션에 EC 개인키·공개키 주입
실행 위치: HSM 클라이언트 서버
$BYOK unwrap-key-aes-kwp \
--slot "$SLOT" --pin "$CU_PASS" \
--unwrapping-label "kek-$TS" \
--label "ec-prv-$TS" \
--key-type EC \
--in "/tmp/sample-ec-$TS.kwp.b64" \
--sign true
$BYOK import-public \
--slot "$SLOT" --pin "$CU_PASS" \
--key-type EC \
--label "ec-pub-$TS" \
--public-in "/tmp/sample-ec-$TS.pub.pem"
Imported EC private key label=ec-prv-20260806103000
Imported EC public key label=ec-pub-20260806103000 handle={handle}
서명·검증 왕복 테스트
실행 위치: HSM 클라이언트 서버
$BYOK sign \
--slot "$SLOT" --pin "$CU_PASS" \
--label "ec-prv-$TS" \
--key-type EC \
--text "byok signature test" \
--out "/tmp/ec-sig-$TS.b64"
$BYOK verify \
--slot "$SLOT" --pin "$CU_PASS" \
--key-type EC \
--public-in "/tmp/sample-ec-$TS.pub.pem" \
--text "byok signature test" \
--in "/tmp/ec-sig-$TS.b64"
ECDSA 서명은 DER로 인코딩된 ASN.1 구조여서 서명값에 따라 길이가 달라집니다. P-256 키의 경우 70~72바이트가 나옵니다.
signatureBytes=72
Output file: /tmp/ec-sig-20260806103000.b64
verified=true
주입한 키 조회
주입한 키가 실제로 영구(token) 객체로 기록됐는지, 의도한 용도 속성이 붙었는지 확인합니다.
실행 위치: HSM 클라이언트 서버
$BYOK show --slot "$SLOT" --pin "$CU_PASS" --label "aes-$TS"
$BYOK show --slot "$SLOT" --pin "$CU_PASS" --label "rsa-prv-$TS"
$BYOK show --slot "$SLOT" --pin "$CU_PASS" --label "ec-prv-$TS"
대칭키의 출력 예시입니다.
found=true label=aes-20260806103000 persistent=true
CKA_TOKEN=true
CKA_PRIVATE=true
CKA_SENSITIVE=true
CKA_EXTRACTABLE=false
CKA_ENCRYPT=true
CKA_DECRYPT=true
CKA_SIGN=true
CKA_VERIFY=true
CKA_WRAP=true
CKA_UNWRAP=false
AES 키에 CKA_SIGN/CKA_VERIFY(HMAC 용도)와 CKA_WRAP이 켜져 있는 것은 LunaProvider의 unwrap 기본 템플릿 때문입니다. 주입할 때 지정하지 않은 속성은 이 기본값을 따릅니다. 암복호화 전용으로 쓸 키라면 운영 적용 시 조정 항목을 참고해 불필요한 용도를 내리십시오.
공개키는 label로 조회되지 않습니다. 주입 시 출력된 handle을 사용하십시오.
$BYOK show --slot "$SLOT" --pin "$CU_PASS" --handle {handle}
파티션 안의 모든 객체를 handle과 함께 나열하려면 LunaClient CLI를 사용하십시오. show는 대상 하나를 확인하는 용도입니다.
/usr/safenet/lunaclient/bin/cmu list -slot "$SLOT" -password "$CU_PASS"
lunacm:> partition contents
정리
개인키 주입에 사용한 임시 KEK는 주입이 끝나면 더 이상 필요하지 않으므로 삭제해 주십시오.
실행 위치: HSM 클라이언트 서버
$BYOK destroy --slot "$SLOT" --pin "$CU_PASS" --label "kek-$TS"
Destroyed handle={handle}
wrapping 키쌍(byok-wrap-$TS-pub / -prv)은 이후 추가 주입에 재사용할 수 있으므로 남겨두어도 됩니다.
키 보유 측의 평문 키 파일과 중간 산출물도 삭제하십시오.
실행 위치: 키 보유 측 작업
이 예제에서는 같은 서버에서 실행합니다.
rm -f "/tmp/sample-rsa-$TS.pem" "/tmp/sample-rsa-$TS.pk8.der" \
"/tmp/sample-ec-$TS.pem" "/tmp/sample-ec-$TS.pk8.der" \
"/tmp/sample-aes-$TS.bin" \
"/tmp/kek-$TS.bin" "/tmp/kek-$TS.oaep.bin" \
"/tmp/sample-aes-$TS.oaep.bin"
이 문서의 절차를 시험 삼아 실행하신 경우, 주입한 키도 정리하실 수 있습니다.
$BYOK destroy --slot "$SLOT" --pin "$CU_PASS" --label "aes-$TS"
$BYOK destroy --slot "$SLOT" --pin "$CU_PASS" --label "rsa-prv-$TS"
$BYOK destroy --slot "$SLOT" --pin "$CU_PASS" --label "ec-prv-$TS"
공개키는 label로 조회되지 않아 destroy --label로 삭제할 수 없습니다. cmu 로 handle을 확인한 뒤 lunacm/ckdemo로 삭제하십시오.
문제 해결
파티션 로그인 실패
Partition login failed: slot=... 오류가 발생합니다.
원인
SLOT/CU_PASS를 실제 값으로 바꾸지 않았거나, 슬롯 번호나 비밀번호가 틀린 경우 발생합니다.
해결 방법
lunacm:> slot list로 슬롯 번호를, Crypto User 비밀번호를 다시 확인하십시오.
LunaProvider 로드 실패
ClassNotFoundException: com.safenetinc.luna.provider.LunaProvider 또는 UnsatisfiedLinkError 오류가 발생합니다.
원인
LunaClient가 설치되어 있지 않거나, javac/java 명령의 -cp/-Djava.library.path 경로가 실제 설치 경로(/usr/safenet/lunaclient/jsp/lib/)와 다른 경우 발생합니다.
해결 방법
LunaClient가 설치되어 있는지 확인해 주십시오.
설치 경로가 정확하게 적용되어 있는지 확인해 주십시오.
unwrap 실패
unwrap 단계에서 CKR_ENCRYPTED_DATA_INVALID 또는 CKR_WRAPPED_KEY_INVALID 오류가 발생합니다.
원인
wrapped blob을 만들 때 사용한 파라미터가 HSM 쪽과 다른 경우입니다.
해결 방법
다음을 확인해 주십시오.
- OpenSSL 명령에
-pkeyopt rsa_oaep_md:sha256과-pkeyopt rsa_mgf1_md:sha256이 모두 들어갔는지 (OpenSSL 기본값은 SHA-1입니다) - 감쌀 때 사용한 공개키 PEM이
--unwrapping-label로 지정한 개인키와 같은 키쌍인지 - base64 파일이 전송 과정에서 잘리지 않았는지
주입은 성공했는데 서명에서 CKR_KEY_FUNCTION_NOT_PERMITTED
파티션 정책 17(Allow signing with non-local keys)이 꺼져 있으면 파티션 밖에서 만들어진 키로 서명할 수 없습니다. lunacm:> partition showPolicies -verbose로 확인하십시오. 이 정책은 켜는 방향이 비파괴적이므로 기존 키를 잃지 않고 켤 수 있습니다. 확인·변경 방법은 파티션 정책 요구사항을 참고해 ㅈ
주입은 성공했는데 복호화에서 CKR_KEY_FUNCTION_NOT_PERMITTED
주입할 때 --decrypt true 를 지정했는지 확인하십시오. show --label <label> 로 CKA_DECRYPT 값을 직접 볼 수 있습니다. 주입 명령 출력에 [warn] failed to set attribute=CKA_DECRYPT 가 있었다면 속성이 반영되지 않은 것이므로, destroy 후 다시 주입하십시오.
컴파일 인코딩 오류
컴파일 시 error: unmappable character (0xED) for encoding US-ASCII 오류가 발생합니다.
원인
javac에 -encoding UTF-8을 지정하지 않은 경우입니다. 서버 로케일이 C/POSIX이면 javac의 기본 소스 인코딩이 US-ASCII가 되어 소스의 한글 주석을 읽지 못합니다.
해결 방법
컴파일 명령대로 -encoding UTF-8을 넣어 다시 컴파일해 주십시오.
파일이 UTF-8로 저장됐는지도 함께 확인해 주십시오.
file HsmByok.java
# HsmByok.java: Unicode text, UTF-8 text <- 이렇게 나와야 합니다
공개키를 주입했는데 show --label이 found=false
공개키를 주입했는데 show --label이 found=false 오류가 발생합니다.
원인
정상입니다. LunaProvider의 label 기반 탐색 API는 공개키 객체를 검색하지 않습니다. 주입 시 출력된 handle로 조회해 주십시오.
$BYOK show --slot "$SLOT" --pin "$CU_PASS" --handle {handle}
파티션 안의 공개키를 handle과 함께 확인하려면 LunaClient CLI를 사용해 주십시오.
/usr/safenet/lunaclient/bin/cmu list -slot "$SLOT" -password "$CU_PASS"
KWP미지원
Cipher.getInstance("AES/KWP/NoPadding")에서 NoSuchAlgorithmException 오류가 발생합니다.
원인
키 보유 측 헬퍼(KwpWrap)를 JDK 16 이하에서 실행한 경우입니다. AES/KWP/NoPadding은 JDK 17부터 표준 제공됩니다.
해결 방법
JDK 17 이상에서 실행하십시오.
운영 적용 시 조정 항목
이 문서의 예제는 절차 확인을 위한 최소 구성입니다. 운영 환경에 적용할 때는 다음을 함께 고려하십시오.
- label 대신
CKA_ID기준 관리: label은 중복이 허용되므로, 테넌트/키 ID 기반의CKA_ID를 함께 지정해 중복 주입과 재시도를 제어하십시오. - 키 메타데이터 별도 보관: 키 유형, mechanism, 해시/MGF, 용도, KCV(키 검증값), 만료 시각, 승인 번호를 HSM 바깥에 함께 기록하십시오. HSM 객체만으로는 어떤 키를 언제 어떤 조건으로 주입했는지 추적할 수 없습니다.
- 주입 전 blob 검증: 전달받은 wrapped blob의 포맷, 크기, 키 유형과 label/
CKA_ID중복 여부를 unwrap 전에 검증하십시오. - 비밀번호 전달 방식: 이 문서의 예제는 CU 비밀번호를 명령행 인자로 전달합니다. 명령행 인자는 프로세스 목록에 노출되므로, 운영 코드에서는 환경 변수나 별도의 비밀 관리 체계를 사용하십시오.
- 불필요한 용도 속성 정리: 주입할 때 지정하지 않은 속성은 LunaProvider의 unwrap 기본 템플릿을 따릅니다. 실측 기준으로 주입한 개인키는
CKA_UNWRAP=true, 주입한 대칭키는CKA_SIGN/CKA_VERIFY/CKA_WRAP=true로 만들어집니다. 용도를 좁히려면show로 현재 값을 확인한 뒤 필요 없는 속성을 내리십시오. - 평문 키 파일 정리: 키 보유 측에서 사용한 평문 키와 임시 KEK 파일은 작업 직후 삭제하십시오.
다음 단계
- 주입 원리와 지원 범위 확인: BYOK(키 주입) 개요