Merge branch 'feature/hsm-integration-jdk8' into master
This commit is contained in:
@@ -0,0 +1,266 @@
|
|||||||
|
package com.eactive.eai.common.hsm;
|
||||||
|
|
||||||
|
import java.beans.PropertyChangeEvent;
|
||||||
|
import java.beans.PropertyChangeListener;
|
||||||
|
import java.security.KeyStore;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.Provider;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.security.cert.Certificate;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
import javax.annotation.PostConstruct;
|
||||||
|
import javax.crypto.Cipher;
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
|
import javax.crypto.spec.IvParameterSpec;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import com.eactive.eai.common.property.PropManager;
|
||||||
|
import com.eactive.eai.common.util.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SafeNet ProtectServer HSM 암복호화 서비스 (JDK 8)
|
||||||
|
*
|
||||||
|
* [키 조회]
|
||||||
|
* getPublicKey - 공개키 반환 (외부 노출). 최초 1회만 HSM 통신, 이후 캐시 반환.
|
||||||
|
* getSecretKey - AES 대칭키 반환 (CKA_EXTRACTABLE=true 필요). 최초 1회만 HSM 통신, 이후 캐시 반환.
|
||||||
|
*
|
||||||
|
* [암복호화]
|
||||||
|
* encryptRsa - 공개키로 JVM 소프트웨어 암호화 (HSM 불필요)
|
||||||
|
* decryptRsa - alias 기반, HSM 내부 복호화 (매 호출마다 HSM 통신 발생 - 불가피)
|
||||||
|
* encryptAes - SecretKey 객체로 AES/CBC 암호화
|
||||||
|
* decryptAes - SecretKey 객체로 AES/CBC 복호화
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class HsmCryptoService implements PropertyChangeListener {
|
||||||
|
|
||||||
|
static Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT);
|
||||||
|
|
||||||
|
private static final String RSA_ALGORITHM = "RSA/ECB/PKCS1Padding";
|
||||||
|
private static final String AES_ALGORITHM = "AES/CBC/PKCS5Padding";
|
||||||
|
|
||||||
|
private static final String PROP_GROUP = "HSM";
|
||||||
|
private static final String PROP_CACHE_RELOAD_YN = "CACHE_RELOAD_YN";
|
||||||
|
|
||||||
|
// HSM 통신 최소화: 최초 1회 조회 후 JVM 메모리에 캐싱
|
||||||
|
private final ConcurrentHashMap<String, SecretKey> secretKeyCache = new ConcurrentHashMap<String, SecretKey>();
|
||||||
|
private final ConcurrentHashMap<String, PublicKey> publicKeyCache = new ConcurrentHashMap<String, PublicKey>();
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private PropManager propManager;
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void registerPropertyChangeListener() {
|
||||||
|
propManager.addPropertyChangeListener(this);
|
||||||
|
logger.warn("HsmCryptoService] PropManager PropertyChangeListener 등록 완료");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 관리포털에서 PropManager.reload("HSM") 호출 시 이벤트를 수신한다.
|
||||||
|
* HSM.CACHE_RELOAD_YN = Y 이면 키 캐시를 초기화한다.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void propertyChange(PropertyChangeEvent evt) {
|
||||||
|
if (!PROP_GROUP.equals(evt.getPropertyName())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String reloadYn = propManager.getProperty(PROP_GROUP, PROP_CACHE_RELOAD_YN, "N");
|
||||||
|
if ("Y".equalsIgnoreCase(reloadYn)) {
|
||||||
|
clearKeyCache();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// 키 조회 (외부 노출)
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HSM KeyStore 에서 공개키를 반환합니다. 최초 1회만 HSM 통신하고 이후 캐시를 반환합니다.
|
||||||
|
*/
|
||||||
|
public PublicKey getPublicKey(String keyAlias) throws HsmException {
|
||||||
|
checkReady();
|
||||||
|
PublicKey cached = publicKeyCache.get(keyAlias);
|
||||||
|
if (cached != null) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Certificate cert = HsmManager.getInstance().getKeyStore().getCertificate(keyAlias);
|
||||||
|
if (cert == null) {
|
||||||
|
throw new HsmException("인증서를 찾을 수 없습니다. alias=" + keyAlias);
|
||||||
|
}
|
||||||
|
PublicKey key = cert.getPublicKey();
|
||||||
|
publicKeyCache.put(keyAlias, key);
|
||||||
|
logger.warn("HsmCryptoService] 공개키 캐시 등록: alias=" + keyAlias);
|
||||||
|
return key;
|
||||||
|
} catch (HsmException e) {
|
||||||
|
throw e;
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new HsmException("공개키 조회 실패: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HSM KeyStore 에서 AES 대칭키를 반환합니다. 최초 1회만 HSM 통신하고 이후 캐시를 반환합니다.
|
||||||
|
* HSM 키 생성 시 CKA_EXTRACTABLE=true (ctkmu -x 플래그) 로 생성된 키만 반환됩니다.
|
||||||
|
*/
|
||||||
|
public SecretKey getSecretKey(String keyAlias) throws HsmException {
|
||||||
|
checkReady();
|
||||||
|
SecretKey cached = secretKeyCache.get(keyAlias);
|
||||||
|
if (cached != null) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
KeyStore keyStore = HsmManager.getInstance().getKeyStore();
|
||||||
|
java.security.Key key = keyStore.getKey(keyAlias, null);
|
||||||
|
if (!(key instanceof SecretKey)) {
|
||||||
|
throw new HsmException("AES 키를 찾을 수 없습니다 (CKA_EXTRACTABLE=true 확인 필요). alias=" + keyAlias);
|
||||||
|
}
|
||||||
|
SecretKey secretKey = (SecretKey) key;
|
||||||
|
secretKeyCache.put(keyAlias, secretKey);
|
||||||
|
logger.warn("HsmCryptoService] 대칭키 캐시 등록: alias=" + keyAlias);
|
||||||
|
return secretKey;
|
||||||
|
} catch (HsmException e) {
|
||||||
|
throw e;
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new HsmException("AES 키 조회 실패: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 캐시를 비웁니다. HSM 키 교체 후 재로드가 필요할 때 호출합니다. */
|
||||||
|
public void clearKeyCache() {
|
||||||
|
secretKeyCache.clear();
|
||||||
|
publicKeyCache.clear();
|
||||||
|
logger.warn("HsmCryptoService] 키 캐시 초기화 완료");
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// RSA
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RSA 암호화 - HSM 인증서의 공개키로 JVM 소프트웨어 암호화.
|
||||||
|
* 공개키는 HSM 에서 추출하므로 HsmManager 가 초기화되어 있어야 합니다.
|
||||||
|
*/
|
||||||
|
public byte[] encryptRsa(String keyAlias, byte[] plaintext) throws HsmException {
|
||||||
|
return encryptRsa(getPublicKey(keyAlias), plaintext);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RSA 암호화 - 전달받은 공개키로 JVM 소프트웨어 암호화.
|
||||||
|
* HSM 없이도 호출 가능합니다.
|
||||||
|
*/
|
||||||
|
public byte[] encryptRsa(PublicKey publicKey, byte[] plaintext) throws HsmException {
|
||||||
|
try {
|
||||||
|
// Provider 미명시 → JVM 소프트웨어 Provider(SunRsaSign) 자동 선택
|
||||||
|
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
|
||||||
|
return cipher.doFinal(plaintext);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new HsmException("RSA 암호화 실패: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RSA 복호화 - 개인키 핸들을 HSM 에서 가져와 HSM 내부에서 복호화.
|
||||||
|
* HsmManager 가 초기화되지 않은 경우 JVM 소프트웨어로 fallback 합니다.
|
||||||
|
*
|
||||||
|
* @param keyAlias HSM KeyStore 의 개인키 별칭
|
||||||
|
* @param pin HSM 슬롯 PIN (null 이면 HsmManager 초기화 시 사용한 PIN 재사용)
|
||||||
|
* @param ciphertext 암호문 바이트
|
||||||
|
*/
|
||||||
|
public byte[] decryptRsa(String keyAlias, char[] pin, byte[] ciphertext) throws HsmException {
|
||||||
|
try {
|
||||||
|
HsmManager hsmManager = HsmManager.getInstance();
|
||||||
|
|
||||||
|
if (hsmManager.isReady()) {
|
||||||
|
// HSM 내부 복호화: pkcs11Provider 명시 필수
|
||||||
|
Provider pkcs11Provider = hsmManager.getPkcs11Provider();
|
||||||
|
java.security.Key key = hsmManager.getKeyStore().getKey(keyAlias, pin);
|
||||||
|
if (!(key instanceof PrivateKey)) {
|
||||||
|
throw new HsmException("개인키를 찾을 수 없습니다. alias=" + keyAlias);
|
||||||
|
}
|
||||||
|
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM, pkcs11Provider);
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, (PrivateKey) key);
|
||||||
|
return cipher.doFinal(ciphertext);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
throw new HsmException("RSA 복호화 불가: HsmManager 가 초기화되지 않았습니다. alias=" + keyAlias);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (HsmException e) {
|
||||||
|
throw e;
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new HsmException("RSA 복호화 실패: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// AES
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AES/CBC/PKCS5Padding 암호화.
|
||||||
|
* extractable 키: provider=null 로 JVM 소프트웨어 암호화.
|
||||||
|
* non-extractable 키: provider 에 pkcs11Provider 를 전달해야 합니다.
|
||||||
|
*
|
||||||
|
* @param secretKey 대칭키 (HSM 추출 키 또는 소프트웨어 키)
|
||||||
|
* @param iv 초기화 벡터 (16바이트)
|
||||||
|
* @param plaintext 평문 바이트
|
||||||
|
* @param provider null 이면 JVM 기본 Provider 사용
|
||||||
|
*/
|
||||||
|
public byte[] encryptAes(SecretKey secretKey, byte[] iv, byte[] plaintext, Provider provider) throws HsmException {
|
||||||
|
try {
|
||||||
|
Cipher cipher = (provider != null)
|
||||||
|
? Cipher.getInstance(AES_ALGORITHM, provider)
|
||||||
|
: Cipher.getInstance(AES_ALGORITHM);
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv));
|
||||||
|
return cipher.doFinal(plaintext);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new HsmException("AES 암호화 실패: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** AES 암호화 - extractable 키 전용 (JVM 소프트웨어 Provider 사용). */
|
||||||
|
public byte[] encryptAes(SecretKey secretKey, byte[] iv, byte[] plaintext) throws HsmException {
|
||||||
|
return encryptAes(secretKey, iv, plaintext, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AES/CBC/PKCS5Padding 복호화.
|
||||||
|
* extractable 키: provider=null 로 JVM 소프트웨어 복호화.
|
||||||
|
* non-extractable 키: provider 에 pkcs11Provider 를 전달해야 합니다.
|
||||||
|
*
|
||||||
|
* @param secretKey 대칭키 (HSM 추출 키 또는 소프트웨어 키)
|
||||||
|
* @param iv 초기화 벡터 (16바이트)
|
||||||
|
* @param ciphertext 암호문 바이트
|
||||||
|
* @param provider null 이면 JVM 기본 Provider 사용
|
||||||
|
*/
|
||||||
|
public byte[] decryptAes(SecretKey secretKey, byte[] iv, byte[] ciphertext, Provider provider) throws HsmException {
|
||||||
|
try {
|
||||||
|
Cipher cipher = (provider != null)
|
||||||
|
? Cipher.getInstance(AES_ALGORITHM, provider)
|
||||||
|
: Cipher.getInstance(AES_ALGORITHM);
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv));
|
||||||
|
return cipher.doFinal(ciphertext);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new HsmException("AES 복호화 실패: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** AES 복호화 - extractable 키 전용 (JVM 소프트웨어 Provider 사용). */
|
||||||
|
public byte[] decryptAes(SecretKey secretKey, byte[] iv, byte[] ciphertext) throws HsmException {
|
||||||
|
return decryptAes(secretKey, iv, ciphertext, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Private helpers
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
private void checkReady() throws HsmException {
|
||||||
|
if (!HsmManager.getInstance().isReady()) {
|
||||||
|
throw new HsmException("HsmManager 가 초기화되지 않았습니다.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.eactive.eai.common.hsm;
|
||||||
|
|
||||||
|
public class HsmException extends Exception {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
public HsmException(String msg) {
|
||||||
|
super(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public HsmException(String msg, Throwable cause) {
|
||||||
|
super(msg, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
package com.eactive.eai.common.hsm;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.security.KeyStore;
|
||||||
|
import java.security.Provider;
|
||||||
|
import java.security.Security;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import com.eactive.eai.common.exception.ExceptionUtil;
|
||||||
|
import com.eactive.eai.common.lifecycle.Lifecycle;
|
||||||
|
import com.eactive.eai.common.lifecycle.LifecycleException;
|
||||||
|
import com.eactive.eai.common.lifecycle.LifecycleListener;
|
||||||
|
import com.eactive.eai.common.lifecycle.LifecycleSupport;
|
||||||
|
import com.eactive.eai.common.property.PropManager;
|
||||||
|
import com.eactive.eai.common.util.ApplicationContextProvider;
|
||||||
|
import com.eactive.eai.common.util.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SafeNet ProtectServer HSM 연동 관리자 (JDK 8 / SunPKCS11)
|
||||||
|
*
|
||||||
|
* PropManager 그룹 "HSM" 에서 읽는 키:
|
||||||
|
* PKCS11_CONFIG - pkcs11.cfg 파일 내용 (name/library/slot 설정을 DB에 직접 저장)
|
||||||
|
* PIN - HSM 슬롯 PIN
|
||||||
|
*
|
||||||
|
* PKCS11_CONFIG 값 예시 (개행은 \n 으로 입력):
|
||||||
|
* name = ProtectServer
|
||||||
|
* library = /opt/safenet/protecttoolkit5/ptk/lib/libcryptoki.so
|
||||||
|
* slot = 0
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class HsmManager implements Lifecycle {
|
||||||
|
|
||||||
|
static Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT);
|
||||||
|
|
||||||
|
private static final String GROUP_NAME = "HSM";
|
||||||
|
private static final String PROP_CONFIG = "PKCS11_CONFIG";
|
||||||
|
private static final String PROP_PIN = "PIN";
|
||||||
|
|
||||||
|
private Provider pkcs11Provider;
|
||||||
|
private KeyStore keyStore;
|
||||||
|
private boolean started;
|
||||||
|
|
||||||
|
private final LifecycleSupport lifecycle = new LifecycleSupport(this);
|
||||||
|
|
||||||
|
private HsmManager() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HsmManager getInstance() {
|
||||||
|
return ApplicationContextProvider.getContext().getBean(HsmManager.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start() throws LifecycleException {
|
||||||
|
if (started) {
|
||||||
|
throw new LifecycleException("RECEAIHSM001");
|
||||||
|
}
|
||||||
|
lifecycle.fireLifecycleEvent(STARTING_EVENT, this);
|
||||||
|
|
||||||
|
try {
|
||||||
|
init();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new LifecycleException(ExceptionUtil.getErrorCode(e, "RECEAIHSM002"));
|
||||||
|
}
|
||||||
|
|
||||||
|
started = true;
|
||||||
|
lifecycle.fireLifecycleEvent(STARTED_EVENT, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void init() throws Exception {
|
||||||
|
String configContent = PropManager.getInstance().getProperty(GROUP_NAME, PROP_CONFIG);
|
||||||
|
String pin = PropManager.getInstance().getProperty(GROUP_NAME, PROP_PIN);
|
||||||
|
|
||||||
|
if (configContent == null || configContent.trim().isEmpty()) {
|
||||||
|
logger.warn("HsmManager] PKCS11_CONFIG 가 설정되지 않았습니다. HSM 초기화를 건너뜁니다.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pkcs11Provider = createProvider(configContent.trim().replace("\\n", "\n"));
|
||||||
|
|
||||||
|
// 이미 등록된 Provider 가 있으면 제거 후 재등록
|
||||||
|
Provider existing = Security.getProvider(pkcs11Provider.getName());
|
||||||
|
if (existing != null) {
|
||||||
|
Security.removeProvider(existing.getName());
|
||||||
|
}
|
||||||
|
Security.addProvider(pkcs11Provider);
|
||||||
|
|
||||||
|
keyStore = KeyStore.getInstance("PKCS11", pkcs11Provider);
|
||||||
|
char[] pinChars = (pin != null) ? pin.toCharArray() : null;
|
||||||
|
keyStore.load(null, pinChars);
|
||||||
|
|
||||||
|
logger.warn("HsmManager] 초기화 완료. Provider=" + pkcs11Provider.getName());
|
||||||
|
|
||||||
|
java.util.Enumeration<String> aliases = keyStore.aliases();
|
||||||
|
StringBuilder aliasList = new StringBuilder();
|
||||||
|
while (aliases.hasMoreElements()) {
|
||||||
|
if (aliasList.length() > 0) aliasList.append(", ");
|
||||||
|
aliasList.append(aliases.nextElement());
|
||||||
|
}
|
||||||
|
logger.warn("HsmManager] HSM 키 목록: [" + aliasList + "]");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JDK 버전에 따라 SunPKCS11 Provider 를 생성한다.
|
||||||
|
*
|
||||||
|
* JDK 8 : SunPKCS11(InputStream) 생성자를 리플렉션으로 호출
|
||||||
|
* JDK 9+: Provider.configure(configFilePath) 를 리플렉션으로 호출
|
||||||
|
*
|
||||||
|
* 두 경로 모두 리플렉션을 사용하므로 컴파일 타임에 JDK 버전 의존성이 없다.
|
||||||
|
*/
|
||||||
|
static Provider createProvider(String cfgContent) throws Exception {
|
||||||
|
String javaVersion = System.getProperty("java.version");
|
||||||
|
if (javaVersion.startsWith("1.")) {
|
||||||
|
// JDK 8: new SunPKCS11(InputStream)
|
||||||
|
InputStream is = new ByteArrayInputStream(cfgContent.getBytes("UTF-8"));
|
||||||
|
return (Provider) Class.forName("sun.security.pkcs11.SunPKCS11")
|
||||||
|
.getConstructor(InputStream.class)
|
||||||
|
.newInstance(is);
|
||||||
|
} else {
|
||||||
|
// JDK 9+: Security.getProvider("SunPKCS11").configure(configFilePath)
|
||||||
|
// configure() 는 JDK 9 에서 추가된 메서드이므로 리플렉션으로 호출
|
||||||
|
File tmp = File.createTempFile("pkcs11-hsm-", ".cfg");
|
||||||
|
tmp.deleteOnExit();
|
||||||
|
Files.write(tmp.toPath(), cfgContent.getBytes("UTF-8"));
|
||||||
|
Provider base = Security.getProvider("SunPKCS11");
|
||||||
|
Method configure = Provider.class.getMethod("configure", String.class);
|
||||||
|
return (Provider) configure.invoke(base, tmp.getAbsolutePath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stop() throws LifecycleException {
|
||||||
|
if (!started) {
|
||||||
|
throw new LifecycleException("RECEAIHSM003");
|
||||||
|
}
|
||||||
|
lifecycle.fireLifecycleEvent(STOPING_EVENT, this);
|
||||||
|
|
||||||
|
if (pkcs11Provider != null) {
|
||||||
|
Security.removeProvider(pkcs11Provider.getName());
|
||||||
|
}
|
||||||
|
pkcs11Provider = null;
|
||||||
|
keyStore = null;
|
||||||
|
started = false;
|
||||||
|
|
||||||
|
lifecycle.fireLifecycleEvent(STOPPED_EVENT, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addLifecycleListener(LifecycleListener listener) {
|
||||||
|
lifecycle.addLifecycleListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LifecycleListener[] findLifecycleListeners() {
|
||||||
|
return lifecycle.findLifecycleListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeLifecycleListener(LifecycleListener listener) {
|
||||||
|
lifecycle.removeLifecycleListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isStarted() {
|
||||||
|
return started;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Provider getPkcs11Provider() {
|
||||||
|
return pkcs11Provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public KeyStore getKeyStore() {
|
||||||
|
return keyStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isReady() {
|
||||||
|
return started && pkcs11Provider != null && keyStore != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# SoftHSM2 for Windows - 개발 환경
|
||||||
|
# https://github.com/disig/SoftHSM2-for-Windows/releases 에서 설치
|
||||||
|
# 토큰 초기화: softhsm2-util --init-token --slot 0 --label "dev-token" --pin 1234 --so-pin 1234
|
||||||
|
name = SoftHSM
|
||||||
|
library = C:/SoftHSM2/lib/softhsm2-x64.dll
|
||||||
|
slotListIndex = 0
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# SafeNet ProtectServer Network HSM - 운영 환경
|
||||||
|
# 환경변수 필요:
|
||||||
|
# ET_HSM_NETCLIENT_SERVERLIST=<HSM 서버 IP>
|
||||||
|
# ET_HSM_NETCLIENT_SERVERPORT=9000
|
||||||
|
# LD_LIBRARY_PATH=/opt/safenet/protecttoolkit5/ptk/lib
|
||||||
|
name = ProtectServer
|
||||||
|
library = /opt/safenet/protecttoolkit5/ptk/lib/libcryptoki.so
|
||||||
|
slot = 0
|
||||||
@@ -0,0 +1,304 @@
|
|||||||
|
package com.eactive.eai.common.hsm;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.junit.jupiter.api.Assumptions.assumeTrue;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.lang.reflect.Constructor;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import javax.crypto.KeyGenerator;
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.AfterAll;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.MethodOrderer;
|
||||||
|
import org.junit.jupiter.api.Order;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.TestMethodOrder;
|
||||||
|
import org.mockito.Mockito;
|
||||||
|
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
||||||
|
import org.springframework.context.support.GenericApplicationContext;
|
||||||
|
|
||||||
|
import com.eactive.eai.common.property.PropManager;
|
||||||
|
import com.eactive.eai.common.util.ApplicationContextProvider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HsmManager / HsmCryptoService 통합 테스트 (JDK 8 / SunPKCS11 / SoftHSM2)
|
||||||
|
*
|
||||||
|
* 사전 조건:
|
||||||
|
* SoftHSM2 설치: https://github.com/disig/SoftHSM2-for-Windows/releases
|
||||||
|
* → C:\SoftHSM2 에 설치 (미설치 시 테스트 자동 건너뜀)
|
||||||
|
*
|
||||||
|
* PropManager 는 DB 없이 Mockito Mock 으로 대체하여 테스트합니다.
|
||||||
|
*/
|
||||||
|
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||||
|
public class HsmManagerServiceTest {
|
||||||
|
|
||||||
|
private static final String SOFTHSM2_DLL = "C:/SoftHSM2/lib/softhsm2-x64.dll";
|
||||||
|
private static final String SOFTHSM2_UTIL = "C:/SoftHSM2/bin/softhsm2-util.exe";
|
||||||
|
private static final String TOKEN_LABEL = "eapim-test";
|
||||||
|
private static final String PIN = "1234";
|
||||||
|
private static final String RSA_ALIAS = "test-rsa-key";
|
||||||
|
private static final String AES_ALIAS = "test-aes-key";
|
||||||
|
|
||||||
|
private static HsmManager hsmManager;
|
||||||
|
private static HsmCryptoService hsmCryptoService;
|
||||||
|
private static GenericApplicationContext springContext;
|
||||||
|
private static File tempCfgFile;
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// 설정 / 해제
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
static void setUp() throws Exception {
|
||||||
|
System.setProperty("SOFTHSM2_CONF", "C:\\SoftHSM2\\etc\\softhsm2.conf");
|
||||||
|
assumeTrue(new File(SOFTHSM2_DLL).exists(),
|
||||||
|
"SoftHSM2 미설치 (C:/SoftHSM2) - 테스트 건너뜀");
|
||||||
|
|
||||||
|
// 1. 토큰 초기화 (없으면 신규 생성, 있으면 기존 사용)
|
||||||
|
ensureTokenInitialized();
|
||||||
|
|
||||||
|
// 2. PKCS11 설정 문자열 구성 (slotListIndex = 0 → 첫 번째 초기화 토큰)
|
||||||
|
String cfgContent = "name = SoftHSM\n"
|
||||||
|
+ "library = " + SOFTHSM2_DLL.replace("\\", "/") + "\n"
|
||||||
|
+ "slotListIndex = 0\n";
|
||||||
|
|
||||||
|
// keytool 호출 시 파일 경로가 필요하므로 임시 파일에도 저장
|
||||||
|
tempCfgFile = File.createTempFile("pkcs11-svc-test-", ".cfg");
|
||||||
|
Files.write(tempCfgFile.toPath(), cfgContent.getBytes("UTF-8"));
|
||||||
|
|
||||||
|
// 3. PropManager Mock - DB 없이 HSM 설정값 반환
|
||||||
|
PropManager mockPropManager = Mockito.mock(PropManager.class);
|
||||||
|
Mockito.when(mockPropManager.getProperty("HSM", "PKCS11_CONFIG")).thenReturn(cfgContent);
|
||||||
|
Mockito.when(mockPropManager.getProperty("HSM", "PIN")).thenReturn(PIN);
|
||||||
|
|
||||||
|
// 4. HsmManager 인스턴스 생성 (private 생성자 → 리플렉션)
|
||||||
|
Constructor<HsmManager> ctor = HsmManager.class.getDeclaredConstructor();
|
||||||
|
ctor.setAccessible(true);
|
||||||
|
hsmManager = ctor.newInstance();
|
||||||
|
|
||||||
|
// 5. Spring ApplicationContext 구성
|
||||||
|
// ApplicationContextProvider.context 를 세팅하여
|
||||||
|
// HsmManager.getInstance() / PropManager.getInstance() 가 동작하게 함
|
||||||
|
springContext = new GenericApplicationContext();
|
||||||
|
springContext.getBeanFactory().registerSingleton("propManager", mockPropManager);
|
||||||
|
springContext.getBeanFactory().registerSingleton("hsmManager", hsmManager);
|
||||||
|
springContext.registerBeanDefinition("applicationContextProvider",
|
||||||
|
BeanDefinitionBuilder.genericBeanDefinition(ApplicationContextProvider.class)
|
||||||
|
.getBeanDefinition());
|
||||||
|
springContext.refresh();
|
||||||
|
|
||||||
|
// 6. HsmManager 시작 (SunPKCS11 Provider 초기화 + KeyStore 오픈)
|
||||||
|
hsmManager.start();
|
||||||
|
assertTrue(hsmManager.isReady(), "HsmManager 초기화 실패");
|
||||||
|
System.out.println("[HsmManager] 초기화 완료. provider=" + hsmManager.getPkcs11Provider().getName());
|
||||||
|
|
||||||
|
// 7. RSA 키쌍 생성 (없는 경우에만)
|
||||||
|
if (!hsmManager.getKeyStore().containsAlias(RSA_ALIAS)) {
|
||||||
|
generateRsaKeyPair();
|
||||||
|
hsmManager.getKeyStore().load(null, PIN.toCharArray()); // keytool 결과 반영
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. AES 키 생성 (없는 경우에만)
|
||||||
|
if (!hsmManager.getKeyStore().containsAlias(AES_ALIAS)) {
|
||||||
|
generateAesKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9. HsmCryptoService 생성 (HsmManager.getInstance() 를 내부에서 사용)
|
||||||
|
hsmCryptoService = new HsmCryptoService();
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterAll
|
||||||
|
static void tearDown() throws Exception {
|
||||||
|
if (hsmManager != null && hsmManager.isStarted()) {
|
||||||
|
hsmManager.stop();
|
||||||
|
}
|
||||||
|
if (springContext != null) {
|
||||||
|
springContext.close();
|
||||||
|
}
|
||||||
|
if (tempCfgFile != null) {
|
||||||
|
tempCfgFile.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// 테스트: HsmManager 상태
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Order(1)
|
||||||
|
void testHsmManagerReady() {
|
||||||
|
assertTrue(hsmManager.isReady());
|
||||||
|
assertNotNull(hsmManager.getPkcs11Provider());
|
||||||
|
assertNotNull(hsmManager.getKeyStore());
|
||||||
|
System.out.println("[HsmManager] isReady=true / provider="
|
||||||
|
+ hsmManager.getPkcs11Provider().getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// 테스트: HsmCryptoService - 키 조회
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Order(2)
|
||||||
|
void testGetPublicKey() throws Exception {
|
||||||
|
PublicKey pk = hsmCryptoService.getPublicKey(RSA_ALIAS);
|
||||||
|
assertNotNull(pk);
|
||||||
|
assertEquals("RSA", pk.getAlgorithm());
|
||||||
|
System.out.println("[HsmCryptoService] getPublicKey: algorithm=" + pk.getAlgorithm()
|
||||||
|
+ " / format=" + pk.getFormat());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Order(3)
|
||||||
|
void testGetSecretKey() throws Exception {
|
||||||
|
SecretKey sk = hsmCryptoService.getSecretKey(AES_ALIAS);
|
||||||
|
assertNotNull(sk);
|
||||||
|
assertEquals("AES", sk.getAlgorithm());
|
||||||
|
System.out.println("[HsmCryptoService] getSecretKey: algorithm=" + sk.getAlgorithm());
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// 테스트: HsmCryptoService - RSA 암복호화
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Order(4)
|
||||||
|
void testEncryptRsaByAlias() throws Exception {
|
||||||
|
byte[] plaintext = "RSA alias 암호화 테스트".getBytes("UTF-8");
|
||||||
|
byte[] ciphertext = hsmCryptoService.encryptRsa(RSA_ALIAS, plaintext);
|
||||||
|
assertNotNull(ciphertext);
|
||||||
|
assertTrue(ciphertext.length > 0);
|
||||||
|
System.out.println("[HsmCryptoService] encryptRsa(alias): ciphertext.length=" + ciphertext.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Order(5)
|
||||||
|
void testEncryptRsaByPublicKey() throws Exception {
|
||||||
|
PublicKey pk = hsmCryptoService.getPublicKey(RSA_ALIAS);
|
||||||
|
byte[] plaintext = "RSA PublicKey 직접 암호화 테스트".getBytes("UTF-8");
|
||||||
|
byte[] ciphertext = hsmCryptoService.encryptRsa(pk, plaintext);
|
||||||
|
assertNotNull(ciphertext);
|
||||||
|
assertTrue(ciphertext.length > 0);
|
||||||
|
System.out.println("[HsmCryptoService] encryptRsa(PublicKey): ciphertext.length=" + ciphertext.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Order(6)
|
||||||
|
void testRsaRoundTrip() throws Exception {
|
||||||
|
byte[] plaintext = "HSM RSA 암복호화 통합 테스트 - HsmCryptoService".getBytes("UTF-8");
|
||||||
|
|
||||||
|
// 암호화: 공개키 → JVM 소프트웨어 (encryptRsa 내부에서 Provider 미명시)
|
||||||
|
byte[] ciphertext = hsmCryptoService.encryptRsa(RSA_ALIAS, plaintext);
|
||||||
|
|
||||||
|
// 복호화: 개인키 핸들 → HSM 내부 (decryptRsa 내부에서 pkcs11Provider 명시)
|
||||||
|
byte[] decrypted = hsmCryptoService.decryptRsa(RSA_ALIAS, PIN.toCharArray(), ciphertext);
|
||||||
|
|
||||||
|
assertArrayEquals(plaintext, decrypted);
|
||||||
|
System.out.println("[HsmCryptoService] RSA 암복호화 성공: " + new String(decrypted, "UTF-8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// 테스트: HsmCryptoService - AES 암복호화
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Order(7)
|
||||||
|
void testAesRoundTrip() throws Exception {
|
||||||
|
byte[] plaintext = "HSM AES 암복호화 통합 테스트 - HsmCryptoService".getBytes("UTF-8");
|
||||||
|
byte[] iv = "1234567890123456".getBytes("UTF-8"); // 16바이트
|
||||||
|
|
||||||
|
SecretKey sk = hsmCryptoService.getSecretKey(AES_ALIAS);
|
||||||
|
|
||||||
|
byte[] ciphertext = hsmCryptoService.encryptAes(sk, iv, plaintext);
|
||||||
|
byte[] decrypted = hsmCryptoService.decryptAes(sk, iv, ciphertext);
|
||||||
|
|
||||||
|
assertArrayEquals(plaintext, decrypted);
|
||||||
|
System.out.println("[HsmCryptoService] AES 암복호화 성공: " + new String(decrypted, "UTF-8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// 테스트: HsmManager Lifecycle - stop / restart
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Order(8)
|
||||||
|
void testHsmManagerStopAndRestart() throws Exception {
|
||||||
|
// stop
|
||||||
|
hsmManager.stop();
|
||||||
|
assertFalse(hsmManager.isStarted());
|
||||||
|
assertFalse(hsmManager.isReady());
|
||||||
|
System.out.println("[HsmManager] stop 완료");
|
||||||
|
|
||||||
|
// restart - PropManager Mock 에서 재설정 읽어 재초기화
|
||||||
|
hsmManager.start();
|
||||||
|
assertTrue(hsmManager.isStarted());
|
||||||
|
assertTrue(hsmManager.isReady());
|
||||||
|
System.out.println("[HsmManager] restart 완료. provider="
|
||||||
|
+ hsmManager.getPkcs11Provider().getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// 헬퍼
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
private static void ensureTokenInitialized() throws Exception {
|
||||||
|
ProcessBuilder pb = new ProcessBuilder(
|
||||||
|
SOFTHSM2_UTIL, "--init-token", "--free",
|
||||||
|
"--label", TOKEN_LABEL,
|
||||||
|
"--pin", PIN,
|
||||||
|
"--so-pin", PIN);
|
||||||
|
pb.redirectErrorStream(true);
|
||||||
|
Process p = pb.start();
|
||||||
|
String out = readOutput(p);
|
||||||
|
p.waitFor(10, TimeUnit.SECONDS);
|
||||||
|
System.out.println("[SoftHSM2] init-token: " + out.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void generateRsaKeyPair() throws Exception {
|
||||||
|
ProcessBuilder pb = new ProcessBuilder(
|
||||||
|
"keytool", "-genkeypair",
|
||||||
|
"-alias", RSA_ALIAS,
|
||||||
|
"-keyalg", "RSA",
|
||||||
|
"-keysize", "2048",
|
||||||
|
"-storetype", "PKCS11",
|
||||||
|
"-providerclass", "sun.security.pkcs11.SunPKCS11",
|
||||||
|
"-providerarg", tempCfgFile.getAbsolutePath(),
|
||||||
|
"-keystore", "NONE",
|
||||||
|
"-storepass", PIN,
|
||||||
|
"-dname", "CN=HSM Test, O=eActive, C=KR",
|
||||||
|
"-noprompt");
|
||||||
|
pb.redirectErrorStream(true);
|
||||||
|
Process p = pb.start();
|
||||||
|
String out = readOutput(p);
|
||||||
|
if (!p.waitFor(30, TimeUnit.SECONDS)) {
|
||||||
|
p.destroy();
|
||||||
|
throw new RuntimeException("keytool -genkeypair timeout");
|
||||||
|
}
|
||||||
|
System.out.println("[keytool] genkeypair exit=" + p.exitValue() + ": " + out.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void generateAesKey() throws Exception {
|
||||||
|
KeyGenerator kg = KeyGenerator.getInstance("AES", hsmManager.getPkcs11Provider());
|
||||||
|
kg.init(256);
|
||||||
|
SecretKey sk = kg.generateKey();
|
||||||
|
hsmManager.getKeyStore().setKeyEntry(AES_ALIAS, sk, null, null);
|
||||||
|
System.out.println("[HsmManager] AES-256 키 생성 완료. alias=" + AES_ALIAS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String readOutput(Process p) throws Exception {
|
||||||
|
InputStream is = p.getInputStream();
|
||||||
|
byte[] buf = new byte[4096];
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
int len;
|
||||||
|
while ((len = is.read(buf)) != -1) {
|
||||||
|
sb.append(new String(buf, 0, len, "UTF-8"));
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,310 @@
|
|||||||
|
package com.eactive.eai.common.hsm;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.junit.jupiter.api.Assumptions.assumeTrue;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.security.Key;
|
||||||
|
import java.security.KeyStore;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.Provider;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.security.Security;
|
||||||
|
import java.security.cert.Certificate;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import javax.crypto.Cipher;
|
||||||
|
import javax.crypto.KeyGenerator;
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
|
import javax.crypto.spec.IvParameterSpec;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.AfterAll;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.MethodOrderer;
|
||||||
|
import org.junit.jupiter.api.Order;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.TestMethodOrder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SoftHSM2 연동 통합 테스트 (JDK 8 / SunPKCS11)
|
||||||
|
*
|
||||||
|
* 사전 조건:
|
||||||
|
* 1. SoftHSM2 설치: https://github.com/disig/SoftHSM2-for-Windows/releases
|
||||||
|
* → SoftHSM2-*-x64.msi 다운로드 후 C:\SoftHSM2 에 설치
|
||||||
|
* 2. 토큰 초기화 (최초 1회):
|
||||||
|
* "C:\SoftHSM2\bin\softhsm2-util.exe" --init-token --free --label "eapim-test" --pin 1234 --so-pin 1234
|
||||||
|
* → 이미 초기화된 경우 @BeforeAll 에서 자동 처리
|
||||||
|
*
|
||||||
|
* SoftHSM2 미설치 시 모든 테스트는 자동으로 건너뜀.
|
||||||
|
*/
|
||||||
|
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||||
|
public class HsmSoftHsm2IntegrationTest {
|
||||||
|
|
||||||
|
private static final String SOFTHSM2_DLL = "C:/SoftHSM2/lib/softhsm2-x64.dll";
|
||||||
|
private static final String SOFTHSM2_UTIL = "C:/SoftHSM2/bin/softhsm2-util.exe";
|
||||||
|
private static final String TOKEN_LABEL = "eapim-test";
|
||||||
|
private static final String PIN = "1234";
|
||||||
|
private static final String RSA_ALIAS = "test-rsa-key";
|
||||||
|
private static final String AES_ALIAS = "test-aes-key";
|
||||||
|
|
||||||
|
private static Provider provider;
|
||||||
|
private static KeyStore keyStore;
|
||||||
|
private static File tempCfgFile;
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// 설정 / 해제
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
static void setUpHsm() throws Exception {
|
||||||
|
System.setProperty("SOFTHSM2_CONF", "C:\\SoftHSM2\\etc\\softhsm2.conf");
|
||||||
|
assumeTrue(new File(SOFTHSM2_DLL).exists(),
|
||||||
|
"SoftHSM2 미설치 (C:/SoftHSM2) - 테스트 건너뜀");
|
||||||
|
|
||||||
|
// 1. 토큰 초기화 (이미 존재하면 출력만 남기고 계속 진행)
|
||||||
|
initToken();
|
||||||
|
|
||||||
|
// 2. pkcs11 설정 내용 (slotListIndex = 0 → 첫 번째 초기화된 토큰 사용)
|
||||||
|
String cfgContent = "name = SoftHSM\n"
|
||||||
|
+ "library = " + SOFTHSM2_DLL.replace("\\", "/") + "\n"
|
||||||
|
+ "slotListIndex = 0\n";
|
||||||
|
|
||||||
|
// keytool 은 파일 경로를 요구하므로 임시 파일에도 저장
|
||||||
|
tempCfgFile = File.createTempFile("pkcs11-test-", ".cfg");
|
||||||
|
Files.write(tempCfgFile.toPath(), cfgContent.getBytes("UTF-8"));
|
||||||
|
|
||||||
|
// 3. SunPKCS11 Provider 초기화 (JDK 8 / JDK 9+ 공용)
|
||||||
|
provider = HsmManager.createProvider(cfgContent);
|
||||||
|
|
||||||
|
Provider existing = Security.getProvider(provider.getName());
|
||||||
|
if (existing != null) {
|
||||||
|
Security.removeProvider(existing.getName());
|
||||||
|
}
|
||||||
|
Security.addProvider(provider);
|
||||||
|
|
||||||
|
// 4. PKCS11 KeyStore 오픈
|
||||||
|
keyStore = KeyStore.getInstance("PKCS11", provider);
|
||||||
|
keyStore.load(null, PIN.toCharArray());
|
||||||
|
|
||||||
|
// 5. RSA 키쌍 생성 (keytool 경유 → 자체서명 인증서 포함)
|
||||||
|
if (!keyStore.containsAlias(RSA_ALIAS)) {
|
||||||
|
generateRsaKeyPair();
|
||||||
|
keyStore.load(null, PIN.toCharArray()); // keytool 결과 반영
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. AES 키 생성 (KeyGenerator 경유)
|
||||||
|
if (!keyStore.containsAlias(AES_ALIAS)) {
|
||||||
|
generateAesKey();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterAll
|
||||||
|
static void tearDown() {
|
||||||
|
if (provider != null) {
|
||||||
|
Security.removeProvider(provider.getName());
|
||||||
|
}
|
||||||
|
if (tempCfgFile != null) {
|
||||||
|
tempCfgFile.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// 테스트: Provider / KeyStore
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Order(1)
|
||||||
|
void testProviderInitialized() {
|
||||||
|
assertNotNull(provider);
|
||||||
|
assertTrue(provider.getName().startsWith("SunPKCS11"),
|
||||||
|
"예상 Provider 이름: SunPKCS11-*, 실제: " + provider.getName());
|
||||||
|
System.out.println("[HSM] Provider: " + provider.getName()
|
||||||
|
+ " / version: " + provider.getVersion());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Order(2)
|
||||||
|
void testKeyStoreContainsRsaAlias() throws Exception {
|
||||||
|
assertTrue(keyStore.containsAlias(RSA_ALIAS),
|
||||||
|
"RSA 키 없음. alias=" + RSA_ALIAS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Order(3)
|
||||||
|
void testKeyStoreContainsAesAlias() throws Exception {
|
||||||
|
assertTrue(keyStore.containsAlias(AES_ALIAS),
|
||||||
|
"AES 키 없음. alias=" + AES_ALIAS);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// 테스트: 키 조회 (HsmCryptoService.getPublicKey / getSecretKey 해당)
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Order(4)
|
||||||
|
void testGetPublicKey() throws Exception {
|
||||||
|
Certificate cert = keyStore.getCertificate(RSA_ALIAS);
|
||||||
|
assertNotNull(cert, "인증서 없음. alias=" + RSA_ALIAS);
|
||||||
|
|
||||||
|
PublicKey pk = cert.getPublicKey();
|
||||||
|
assertNotNull(pk);
|
||||||
|
assertEquals("RSA", pk.getAlgorithm());
|
||||||
|
System.out.println("[HSM] PublicKey algorithm=" + pk.getAlgorithm()
|
||||||
|
+ " / format=" + pk.getFormat());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Order(5)
|
||||||
|
void testGetSecretKey() throws Exception {
|
||||||
|
Key key = keyStore.getKey(AES_ALIAS, null);
|
||||||
|
assertNotNull(key, "AES 키 없음. alias=" + AES_ALIAS);
|
||||||
|
assertInstanceOf(SecretKey.class, key);
|
||||||
|
assertEquals("AES", key.getAlgorithm());
|
||||||
|
System.out.println("[HSM] SecretKey algorithm=" + key.getAlgorithm());
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// 테스트: RSA 암복호화 (HsmCryptoService.encryptRsa / decryptRsa 해당)
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Order(6)
|
||||||
|
void testRsaEncrypt() throws Exception {
|
||||||
|
PublicKey publicKey = keyStore.getCertificate(RSA_ALIAS).getPublicKey();
|
||||||
|
|
||||||
|
// Provider 미명시 → JVM SunRsaSign 으로 암호화 (HsmCryptoService.encryptRsa 동작)
|
||||||
|
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
|
||||||
|
byte[] ciphertext = cipher.doFinal("테스트 평문".getBytes("UTF-8"));
|
||||||
|
|
||||||
|
assertNotNull(ciphertext);
|
||||||
|
assertTrue(ciphertext.length > 0);
|
||||||
|
System.out.println("[HSM] RSA 암호화 성공. ciphertext.length=" + ciphertext.length
|
||||||
|
+ " / provider=" + cipher.getProvider().getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Order(7)
|
||||||
|
void testRsaEncryptDecryptRoundTrip() throws Exception {
|
||||||
|
byte[] plaintext = "HSM RSA 암복호화 테스트 - eapim-online".getBytes("UTF-8");
|
||||||
|
|
||||||
|
// 암호화: 공개키 → JVM 소프트웨어 (Provider 미명시)
|
||||||
|
PublicKey publicKey = keyStore.getCertificate(RSA_ALIAS).getPublicKey();
|
||||||
|
Cipher encCipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
|
||||||
|
encCipher.init(Cipher.ENCRYPT_MODE, publicKey);
|
||||||
|
byte[] ciphertext = encCipher.doFinal(plaintext);
|
||||||
|
|
||||||
|
// 복호화: 개인키 핸들 → HSM 내부 수행 (pkcs11Provider 명시)
|
||||||
|
PrivateKey privateKey = (PrivateKey) keyStore.getKey(RSA_ALIAS, PIN.toCharArray());
|
||||||
|
Cipher decCipher = Cipher.getInstance("RSA/ECB/PKCS1Padding", provider);
|
||||||
|
decCipher.init(Cipher.DECRYPT_MODE, privateKey);
|
||||||
|
byte[] decrypted = decCipher.doFinal(ciphertext);
|
||||||
|
|
||||||
|
assertArrayEquals(plaintext, decrypted);
|
||||||
|
System.out.println("[HSM] RSA 암복호화 성공."
|
||||||
|
+ " enc.provider=" + encCipher.getProvider().getName()
|
||||||
|
+ " / dec.provider=" + decCipher.getProvider().getName()
|
||||||
|
+ " / plaintext=" + new String(decrypted, "UTF-8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// 테스트: AES 암복호화 (HsmCryptoService.encryptAes / decryptAes 해당)
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Order(8)
|
||||||
|
void testAesEncryptDecryptRoundTrip() throws Exception {
|
||||||
|
byte[] plaintext = "HSM AES 암복호화 테스트 - eapim-online".getBytes("UTF-8");
|
||||||
|
byte[] iv = "1234567890123456".getBytes("UTF-8"); // 16바이트 IV
|
||||||
|
|
||||||
|
SecretKey secretKey = (SecretKey) keyStore.getKey(AES_ALIAS, null);
|
||||||
|
|
||||||
|
// 암호화
|
||||||
|
Cipher encCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||||
|
encCipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv));
|
||||||
|
byte[] ciphertext = encCipher.doFinal(plaintext);
|
||||||
|
|
||||||
|
// 복호화
|
||||||
|
Cipher decCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||||
|
decCipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv));
|
||||||
|
byte[] decrypted = decCipher.doFinal(ciphertext);
|
||||||
|
|
||||||
|
assertArrayEquals(plaintext, decrypted);
|
||||||
|
System.out.println("[HSM] AES 암복호화 성공. plaintext=" + new String(decrypted, "UTF-8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// 헬퍼
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
private static void initToken() throws Exception {
|
||||||
|
ProcessBuilder pb = new ProcessBuilder(
|
||||||
|
SOFTHSM2_UTIL, "--init-token", "--free",
|
||||||
|
"--label", TOKEN_LABEL,
|
||||||
|
"--pin", PIN,
|
||||||
|
"--so-pin", PIN);
|
||||||
|
pb.redirectErrorStream(true);
|
||||||
|
Process p = pb.start();
|
||||||
|
String output = readOutput(p);
|
||||||
|
p.waitFor(10, TimeUnit.SECONDS);
|
||||||
|
// 이미 초기화된 슬롯이 없으면 실패할 수 있으나 무시 (기존 토큰 재사용)
|
||||||
|
System.out.println("[SoftHSM2] init-token: " + output.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* keytool 로 RSA 키쌍 + 자체서명 인증서를 PKCS11 토큰에 생성.
|
||||||
|
* 인증서가 있어야 PKCS11 KeyStore 에서 alias 로 조회 가능.
|
||||||
|
*/
|
||||||
|
private static void generateRsaKeyPair() throws Exception {
|
||||||
|
ProcessBuilder pb = new ProcessBuilder(
|
||||||
|
"keytool", "-genkeypair",
|
||||||
|
"-alias", RSA_ALIAS,
|
||||||
|
"-keyalg", "RSA",
|
||||||
|
"-keysize", "2048",
|
||||||
|
"-storetype", "PKCS11",
|
||||||
|
"-providerclass", "sun.security.pkcs11.SunPKCS11",
|
||||||
|
"-providerarg", tempCfgFile.getAbsolutePath(),
|
||||||
|
"-keystore", "NONE",
|
||||||
|
"-storepass", PIN,
|
||||||
|
"-dname", "CN=HSM Test, O=eActive, C=KR",
|
||||||
|
"-noprompt");
|
||||||
|
pb.redirectErrorStream(true);
|
||||||
|
Process p = pb.start();
|
||||||
|
String output = readOutput(p);
|
||||||
|
boolean finished = p.waitFor(30, TimeUnit.SECONDS);
|
||||||
|
if (!finished) {
|
||||||
|
p.destroy();
|
||||||
|
throw new RuntimeException("keytool -genkeypair timeout");
|
||||||
|
}
|
||||||
|
System.out.println("[keytool] genkeypair exit=" + p.exitValue() + ": " + output.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KeyGenerator 로 AES-256 키를 HSM 토큰에 직접 생성.
|
||||||
|
* SoftHSM2 의 AES 키는 기본적으로 extractable(추출 가능).
|
||||||
|
*/
|
||||||
|
private static void generateAesKey() throws Exception {
|
||||||
|
KeyGenerator kg = KeyGenerator.getInstance("AES", provider);
|
||||||
|
kg.init(256);
|
||||||
|
SecretKey sk = kg.generateKey();
|
||||||
|
keyStore.setKeyEntry(AES_ALIAS, sk, null, null);
|
||||||
|
System.out.println("[HSM] AES-256 키 생성 완료. alias=" + AES_ALIAS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String readOutput(Process p) throws Exception {
|
||||||
|
InputStream is = p.getInputStream();
|
||||||
|
byte[] buf = new byte[4096];
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
int len;
|
||||||
|
while ((len = is.read(buf)) != -1) {
|
||||||
|
sb.append(new String(buf, 0, len, "UTF-8"));
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user