845 lines
33 KiB
Java
845 lines
33 KiB
Java
package com.eactive.eai.common.hsm;
|
|
|
|
import java.beans.PropertyChangeEvent;
|
|
import java.beans.PropertyChangeListener;
|
|
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.AuthProvider;
|
|
import java.security.KeyStore;
|
|
import java.security.KeyStoreException;
|
|
import java.security.NoSuchAlgorithmException;
|
|
import java.security.Provider;
|
|
import java.security.Security;
|
|
import java.security.UnrecoverableKeyException;
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.Collections;
|
|
import java.util.List;
|
|
import java.util.concurrent.Executors;
|
|
import java.util.concurrent.ScheduledExecutorService;
|
|
import java.util.concurrent.ScheduledFuture;
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
import javax.crypto.SecretKey;
|
|
|
|
import org.apache.commons.lang3.StringUtils;
|
|
import org.springframework.stereotype.Component;
|
|
|
|
import com.eactive.eai.agent.encryption.EncryptionManager;
|
|
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.PropGroupVO;
|
|
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 - primary pkcs11.cfg 파일 내용
|
|
* PIN - primary 슬롯 PIN
|
|
* PKCS11_CONFIG_SECONDARY - secondary pkcs11.cfg 파일 내용 (미설정 시 이중화 비활성)
|
|
* PIN_SECONDARY - secondary 슬롯 PIN (미설정 시 PIN 재사용)
|
|
* RELOAD_INTERVAL_MINUTES - KeyStore 주기적 재로드 간격(분)
|
|
*
|
|
* PKCS11_CONFIG 값 예시 (개행은 \n 으로 입력):
|
|
* name = ProtectServer
|
|
* library = /opt/safenet/protecttoolkit5/ptk/lib/libcryptoki.so
|
|
* slot = 0
|
|
*
|
|
* name = SoftHSM
|
|
* library = C:/SoftHSM2/lib/softhsm2-x64.dll
|
|
* slotListIndex = 0
|
|
*
|
|
* ---------------------------------------------------------------------
|
|
* [설정 이중화 / primary 자동 복귀]
|
|
* 기동(init) 과 재연결 시 항상 primary -> secondary 순서로 연결을 시도하고,
|
|
* 최초로 성공한 설정을 사용한다. secondary 로 절체된 뒤에는 주기적 재로드마다
|
|
* primary 재연결을 먼저 시도하므로 primary 가 복구되면 자동으로 되돌아온다.
|
|
*
|
|
* [세션 누적 방지 / 회로차단 로직]
|
|
* 재로드마다 KeyStore.getInstance(...).load(null, pin) 을 새로 호출하면
|
|
* PKCS11 세션(C_OpenSession)이 계속 누적되고, 결국 HSM 파티션의 최대 세션 수를
|
|
* 초과하면서 reload 가 영구적으로 실패한다. 이를 방지하기 위해:
|
|
* 1) primary 로 동작 중이고 설정도 그대로면 "기존 keyStore 인스턴스"에 다시
|
|
* load() 하여 세션을 재사용한다 (= primary 우선 시도와 동일한 의미)
|
|
* 2) 실제 키 조회(probe)로 세션이 살아있는지 검증
|
|
* 3) 재로드가 실패하거나 프로퍼티가 변경되면 primary -> secondary 순서로
|
|
* Provider 를 완전히 재생성
|
|
* 4) 재생성마저 실패하면 마지막으로 성공한 keyStore 를 유지 (서비스 연속성 우선)
|
|
*
|
|
* [프로퍼티 즉시 반영]
|
|
* PropManager 의 PropertyChangeListener 로 등록되어 있어, 관리 포털에서 HSM 그룹을
|
|
* reload 하면 다음 스케줄을 기다리지 않고 즉시 설정을 다시 읽어 재연결을 시도한다.
|
|
* 이 경우에도 신규 연결이 완전히 성공한 뒤에만 keyStore 멤버변수를 교체한다.
|
|
* ---------------------------------------------------------------------
|
|
*/
|
|
@Component
|
|
public class HsmManager implements Lifecycle, PropertyChangeListener {
|
|
|
|
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 static final String PROP_CONFIG_SECONDARY = "PKCS11_CONFIG_SECONDARY";
|
|
private static final String PROP_PIN_SECONDARY = "PIN_SECONDARY";
|
|
private static final String PROP_RELOAD_INTERVAL_MINUTES = "RELOAD_INTERVAL_MINUTES";
|
|
|
|
/** 설정 구분자. HSM 상태 조회 API 응답에도 그대로 노출된다. */
|
|
public static final String CONFIG_PRIMARY = "PRIMARY";
|
|
public static final String CONFIG_SECONDARY = "SECONDARY";
|
|
|
|
private static final long DEFAULT_RELOAD_INTERVAL_MINUTES = 10;
|
|
|
|
private Provider pkcs11Provider;
|
|
private volatile KeyStore keyStore;
|
|
private boolean started;
|
|
|
|
private final LifecycleSupport lifecycle = new LifecycleSupport(this);
|
|
|
|
private volatile char[] pin;
|
|
|
|
/** 현재 연결에 사용 중인 설정. 미연결이면 null. */
|
|
private volatile HsmConfig activeConfig;
|
|
|
|
// ---- 모니터링용 상태값 (HsmStatusController 에서 조회) ----
|
|
private volatile long activeSince;
|
|
private volatile long lastReloadAt;
|
|
private volatile String lastReloadResult;
|
|
private volatile String lastErrorMessage;
|
|
private volatile long lastErrorAt;
|
|
private volatile int reloadSuccessCount;
|
|
private volatile int reloadFailCount;
|
|
private volatile int failoverCount;
|
|
|
|
private volatile long reloadIntervalMinutes = DEFAULT_RELOAD_INTERVAL_MINUTES;
|
|
private ScheduledExecutorService scheduler;
|
|
private ScheduledFuture<?> reloadFuture;
|
|
private volatile boolean propListenerRegistered;
|
|
|
|
private HsmManager() {
|
|
}
|
|
|
|
public static HsmManager getInstance() {
|
|
return ApplicationContextProvider.getContext().getBean(HsmManager.class);
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// 설정 후보 (primary / secondary)
|
|
// -------------------------------------------------------------------------
|
|
|
|
/**
|
|
* 한 번의 연결 시도에 필요한 설정 한 벌. PropManager 에서 읽어 복호화까지 마친 값이다.
|
|
*/
|
|
static final class HsmConfig {
|
|
|
|
final String name;
|
|
final String configContent;
|
|
final char[] pin;
|
|
|
|
HsmConfig(String name, String configContent, char[] pin) {
|
|
this.name = name;
|
|
this.configContent = configContent;
|
|
this.pin = pin;
|
|
}
|
|
|
|
/** PropManager 값이 바뀌었는지 판단하기 위한 비교. */
|
|
boolean sameAs(HsmConfig other) {
|
|
if (other == null) {
|
|
return false;
|
|
}
|
|
return name.equals(other.name)
|
|
&& configContent.equals(other.configContent)
|
|
&& Arrays.equals(pin, other.pin);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 연결 시도 결과. Security 등록 전 상태이며, 완전히 성공한 경우에만
|
|
* applyConnection() 을 통해 멤버변수로 승격된다.
|
|
*/
|
|
private static final class HsmConnection {
|
|
|
|
final HsmConfig config;
|
|
final Provider provider;
|
|
final KeyStore keyStore;
|
|
|
|
HsmConnection(HsmConfig config, Provider provider, KeyStore keyStore) {
|
|
this.config = config;
|
|
this.provider = provider;
|
|
this.keyStore = keyStore;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* PropManager 에서 매번 새로 읽어 primary -> secondary 순서의 연결 후보를 만든다.
|
|
* 스케줄러/프로퍼티 변경 이벤트 모두 이 메서드를 통하므로 변경된 값이 즉시 반영된다.
|
|
* PKCS11_CONFIG 가 비어 있는 후보는 제외한다.
|
|
*/
|
|
private List<HsmConfig> loadConfigCandidates() {
|
|
PropManager propManager = PropManager.getInstance();
|
|
EncryptionManager encManager = EncryptionManager.getInstance();
|
|
|
|
List<HsmConfig> candidates = new ArrayList<>();
|
|
|
|
String primaryConfig = decrypt(encManager, propManager.getProperty(GROUP_NAME, PROP_CONFIG));
|
|
String primaryPin = decrypt(encManager, propManager.getProperty(GROUP_NAME, PROP_PIN));
|
|
if (StringUtils.isNotBlank(primaryConfig)) {
|
|
candidates.add(new HsmConfig(CONFIG_PRIMARY, normalizeConfig(primaryConfig), toPin(primaryPin)));
|
|
}
|
|
|
|
String secondaryConfig = decrypt(encManager, propManager.getProperty(GROUP_NAME, PROP_CONFIG_SECONDARY));
|
|
if (StringUtils.isNotBlank(secondaryConfig)) {
|
|
// PIN_SECONDARY 미설정이면 primary PIN 을 재사용한다 (슬롯만 이중화하는 구성 지원)
|
|
String rawSecondaryPin = propManager.getProperty(GROUP_NAME, PROP_PIN_SECONDARY);
|
|
String secondaryPin = StringUtils.isNotBlank(rawSecondaryPin) ? decrypt(encManager, rawSecondaryPin) : primaryPin;
|
|
candidates.add(new HsmConfig(CONFIG_SECONDARY, normalizeConfig(secondaryConfig), toPin(secondaryPin)));
|
|
}
|
|
|
|
return candidates;
|
|
}
|
|
|
|
private static String decrypt(EncryptionManager encManager, String value) {
|
|
if (value == null) {
|
|
return null;
|
|
}
|
|
return encManager.decryptDBData(value);
|
|
}
|
|
|
|
private static String normalizeConfig(String configContent) {
|
|
return configContent.trim().replace("\\n", "\n");
|
|
}
|
|
|
|
private static char[] toPin(String pinStr) {
|
|
return (pinStr != null) ? pinStr.toCharArray() : null;
|
|
}
|
|
|
|
private static HsmConfig findByName(List<HsmConfig> candidates, String name) {
|
|
if (name == null) {
|
|
return null;
|
|
}
|
|
for (HsmConfig candidate : candidates) {
|
|
if (name.equals(candidate.name)) {
|
|
return candidate;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Lifecycle
|
|
// -------------------------------------------------------------------------
|
|
|
|
@Override
|
|
public void start() throws LifecycleException {
|
|
if (started) {
|
|
throw new LifecycleException("RECEAIHSM001");
|
|
}
|
|
lifecycle.fireLifecycleEvent(STARTING_EVENT, this);
|
|
|
|
try {
|
|
init();
|
|
startReloadScheduler();
|
|
registerPropertyChangeListener();
|
|
} catch (Exception e) {
|
|
throw new LifecycleException(ExceptionUtil.getErrorCode(e, "RECEAIHSM002"));
|
|
}
|
|
|
|
started = true;
|
|
lifecycle.fireLifecycleEvent(STARTED_EVENT, this);
|
|
}
|
|
|
|
private void init() throws Exception {
|
|
|
|
applyReloadInterval();
|
|
|
|
List<HsmConfig> candidates = loadConfigCandidates();
|
|
if (candidates.isEmpty()) {
|
|
logger.warn("HsmManager] PKCS11_CONFIG 가 설정되지 않았습니다. HSM 초기화를 건너뜁니다.");
|
|
return;
|
|
}
|
|
|
|
HsmConnection connection = connectFirstAvailable(candidates);
|
|
applyConnection(connection);
|
|
|
|
logger.warn("HsmManager] 초기화 완료. config=" + connection.config.name
|
|
+ ", Provider=" + connection.provider.getName());
|
|
}
|
|
|
|
/**
|
|
* RELOAD_INTERVAL_MINUTES 를 다시 읽어 적용한다.
|
|
*
|
|
* @return 값이 변경되어 스케줄 재등록이 필요하면 true
|
|
*/
|
|
private boolean applyReloadInterval() {
|
|
long newInterval = reloadIntervalMinutes;
|
|
try {
|
|
String value = PropManager.getInstance().getProperty(GROUP_NAME, PROP_RELOAD_INTERVAL_MINUTES,
|
|
String.valueOf(DEFAULT_RELOAD_INTERVAL_MINUTES));
|
|
if (StringUtils.isNotBlank(value)) {
|
|
newInterval = Long.parseLong(value.trim());
|
|
}
|
|
} catch (Exception e) {
|
|
logger.warn("HsmManager] " + PROP_RELOAD_INTERVAL_MINUTES + " 값이 올바르지 않아 기존 값("
|
|
+ reloadIntervalMinutes + "분)을 유지합니다: " + e.getMessage());
|
|
return false;
|
|
}
|
|
|
|
if (newInterval <= 0) {
|
|
newInterval = DEFAULT_RELOAD_INTERVAL_MINUTES;
|
|
}
|
|
if (newInterval == reloadIntervalMinutes) {
|
|
return false;
|
|
}
|
|
reloadIntervalMinutes = newInterval;
|
|
return true;
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// 연결
|
|
// -------------------------------------------------------------------------
|
|
|
|
/**
|
|
* 후보 설정을 primary -> secondary 순서로 시도하여 최초로 성공한 연결을 반환한다.
|
|
* 각 후보는 Provider 생성 + KeyStore.load + 키 목록 조회(probe)까지 모두 성공해야
|
|
* 정상으로 간주한다. 전부 실패하면 마지막 예외를 던진다.
|
|
*/
|
|
private HsmConnection connectFirstAvailable(List<HsmConfig> candidates) throws Exception {
|
|
Exception lastException = null;
|
|
|
|
for (HsmConfig config : candidates) {
|
|
try {
|
|
HsmConnection connection = connect(config);
|
|
logger.warn("HsmManager] HSM 연결 성공. config=" + config.name
|
|
+ ", Provider=" + connection.provider.getName());
|
|
return connection;
|
|
} catch (Exception e) {
|
|
lastException = e;
|
|
recordError(config.name + " 설정 연결 실패: " + e.getMessage());
|
|
logger.warn("HsmManager] HSM 연결 실패. config=" + config.name + " : " + e.getMessage(), e);
|
|
}
|
|
}
|
|
|
|
if (lastException != null) {
|
|
throw lastException;
|
|
}
|
|
throw new HsmException("HSM 연결 후보 설정이 없습니다.");
|
|
}
|
|
|
|
/**
|
|
* 단일 설정으로 Provider/KeyStore 를 새로 생성한다. Security 에는 아직 등록하지 않으므로
|
|
* 여기서 실패해도 현재 사용 중인 Provider/keyStore 는 영향을 받지 않는다.
|
|
*/
|
|
private HsmConnection connect(HsmConfig config) throws Exception {
|
|
Provider provider = createProvider(config.configContent);
|
|
KeyStore ks = KeyStore.getInstance("PKCS11", provider);
|
|
ks.load(null, config.pin);
|
|
logKeyStore(ks);
|
|
return new HsmConnection(config, provider, ks);
|
|
}
|
|
|
|
/**
|
|
* 신규 연결을 멤버변수로 승격한다. 기존 Provider 는 이 시점에서만 logout/제거된다.
|
|
*/
|
|
private void applyConnection(HsmConnection connection) {
|
|
Provider oldProvider = this.pkcs11Provider;
|
|
HsmConfig oldConfig = this.activeConfig;
|
|
|
|
if (oldProvider != null && oldProvider != connection.provider) {
|
|
if (oldProvider instanceof AuthProvider) {
|
|
try {
|
|
((AuthProvider) oldProvider).logout();
|
|
} catch (Exception logoutEx) {
|
|
logger.warn("HsmManager] 기존 세션 logout 실패(무시하고 진행): " + logoutEx.getMessage());
|
|
}
|
|
}
|
|
Security.removeProvider(oldProvider.getName());
|
|
}
|
|
|
|
// 동일 이름으로 이미 등록된 Provider 가 있으면 제거 후 재등록
|
|
Provider registered = Security.getProvider(connection.provider.getName());
|
|
if (registered != null && registered != connection.provider) {
|
|
Security.removeProvider(registered.getName());
|
|
}
|
|
Security.addProvider(connection.provider);
|
|
|
|
this.pkcs11Provider = connection.provider;
|
|
this.keyStore = connection.keyStore;
|
|
this.activeConfig = connection.config;
|
|
this.pin = connection.config.pin;
|
|
this.activeSince = System.currentTimeMillis();
|
|
|
|
if (oldConfig != null && !oldConfig.name.equals(connection.config.name)) {
|
|
failoverCount++;
|
|
logger.warn("HsmManager] HSM 설정 절체: " + oldConfig.name + " -> " + connection.config.name);
|
|
}
|
|
}
|
|
|
|
private void logKeyStore(KeyStore keyStore)
|
|
throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
|
|
java.util.Enumeration<String> aliases = keyStore.aliases();
|
|
StringBuilder aliasList = new StringBuilder();
|
|
while (aliases.hasMoreElements()) {
|
|
if (aliasList.length() > 0) aliasList.append(", ");
|
|
|
|
String alias = aliases.nextElement();
|
|
aliasList.append(alias);
|
|
|
|
java.security.Key key = keyStore.getKey(alias, null);
|
|
if (key instanceof SecretKey) {
|
|
SecretKey secretKey = (SecretKey) key;
|
|
if (secretKey.getEncoded() == null) {
|
|
logger.warn("HsmManager] HSM - secretKey null {} : [{}]", alias, secretKey);
|
|
}
|
|
}
|
|
}
|
|
logger.warn("HsmManager] HSM 키 목록: [" + aliasList + "]");
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// 주기적 재로드
|
|
// -------------------------------------------------------------------------
|
|
|
|
/**
|
|
* 별도 스레드에서 주기적으로 KeyStore 를 다시 로드하여
|
|
* HSM 에 새로 생성/추가된 키를 인식하도록 한다.
|
|
*/
|
|
private void startReloadScheduler() {
|
|
scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
|
|
Thread t = new Thread(r, "hsm-keystore-reloader");
|
|
t.setDaemon(true);
|
|
return t;
|
|
});
|
|
|
|
reloadFuture = scheduler.scheduleWithFixedDelay(
|
|
this::reloadKeyStoreSafely,
|
|
reloadIntervalMinutes,
|
|
reloadIntervalMinutes,
|
|
TimeUnit.MINUTES
|
|
);
|
|
|
|
logger.warn("HsmManager] KeyStore 주기적 재로드 스케줄러 시작. interval=" + reloadIntervalMinutes + "분");
|
|
}
|
|
|
|
/** RELOAD_INTERVAL_MINUTES 변경 시 스케줄을 다시 등록한다. */
|
|
private synchronized void rescheduleReload() {
|
|
if (scheduler == null || scheduler.isShutdown()) {
|
|
return;
|
|
}
|
|
if (reloadFuture != null) {
|
|
reloadFuture.cancel(false);
|
|
}
|
|
reloadFuture = scheduler.scheduleWithFixedDelay(
|
|
this::reloadKeyStoreSafely,
|
|
reloadIntervalMinutes,
|
|
reloadIntervalMinutes,
|
|
TimeUnit.MINUTES
|
|
);
|
|
logger.warn("HsmManager] KeyStore 재로드 주기 변경 적용: " + reloadIntervalMinutes + "분");
|
|
}
|
|
|
|
/**
|
|
* 스케줄러에서 호출되는 래퍼. 예외가 스케줄러 스레드를 죽이지 않도록 반드시 catch 한다.
|
|
* (ScheduledExecutorService 는 task 에서 예외가 던져지면 이후 스케줄을 자동으로 중단시킨다)
|
|
*/
|
|
private void reloadKeyStoreSafely() {
|
|
try {
|
|
reloadKeyStoreIfNeeded();
|
|
} catch (Throwable t) {
|
|
logger.warn("HsmManager] KeyStore 주기적 재로드 실패: " + t.getMessage(), t);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* KeyStore 를 다시 로드한다. 외부(getSecretKey 등)에서 키 미스 발생 시
|
|
* 즉시 재시도용으로 직접 호출할 수도 있다.
|
|
*
|
|
* 처리 순서:
|
|
* 1) PropManager 에서 설정을 매번 새로 읽는다 (변경값 즉시 반영)
|
|
* 2) 설정이 변경되었거나 아직 연결이 없으면 primary -> secondary 순으로 전체 재연결
|
|
* 3) 현재 secondary 로 동작 중이면 primary 복귀를 먼저 시도
|
|
* 4) 그 외에는 기존 keyStore 인스턴스에 다시 load() (PKCS11 세션 재사용).
|
|
* 실패하면 primary -> secondary 순으로 전체 재연결
|
|
*
|
|
* 어느 경로든 신규 연결이 완전히 성공한 경우에만 keyStore 멤버변수를 교체한다.
|
|
*/
|
|
public synchronized void reloadKeyStoreIfNeeded() throws Exception {
|
|
|
|
List<HsmConfig> candidates = loadConfigCandidates();
|
|
if (candidates.isEmpty()) {
|
|
logger.warn("HsmManager] PKCS11_CONFIG 가 설정되지 않아 재로드를 건너뜁니다.");
|
|
return;
|
|
}
|
|
|
|
HsmConfig active = this.activeConfig;
|
|
HsmConfig currentCandidate = findByName(candidates, active == null ? null : active.name);
|
|
|
|
// 2) 미연결 상태이거나 현재 사용 중인 설정값 자체가 변경된 경우
|
|
if (active == null || pkcs11Provider == null || currentCandidate == null
|
|
|| !currentCandidate.sameAs(active)) {
|
|
logger.warn("HsmManager] HSM 설정 변경 또는 미연결 상태 감지 → 전체 재연결을 수행합니다.");
|
|
reconnect(candidates);
|
|
return;
|
|
}
|
|
|
|
// 3) secondary 로 동작 중이면 primary 복귀를 우선 시도
|
|
HsmConfig preferred = candidates.get(0);
|
|
if (!preferred.name.equals(active.name)) {
|
|
try {
|
|
HsmConnection connection = connect(preferred);
|
|
applyConnection(connection);
|
|
recordReloadSuccess(preferred.name + " 설정으로 복귀 성공");
|
|
logger.warn("HsmManager] " + preferred.name + " 설정으로 복귀했습니다.");
|
|
return;
|
|
} catch (Exception e) {
|
|
logger.warn("HsmManager] " + preferred.name + " 복귀 시도 실패. 현재 " + active.name
|
|
+ " 설정을 유지합니다: " + e.getMessage());
|
|
}
|
|
}
|
|
|
|
// 4) 현재 연결 유지 재로드 (세션 재사용)
|
|
try {
|
|
if (keyStore != null) {
|
|
keyStore.load(null, pin);
|
|
logKeyStore(keyStore);
|
|
logger.warn("HsmManager] KeyStore 재로드 완료 (config=" + active.name + ", 기존 세션 재사용).");
|
|
} else {
|
|
KeyStore ks = KeyStore.getInstance("PKCS11", pkcs11Provider);
|
|
ks.load(null, pin);
|
|
logKeyStore(ks);
|
|
this.keyStore = ks;
|
|
logger.warn("HsmManager] KeyStore 신규 생성 완료 (config=" + active.name + ").");
|
|
}
|
|
recordReloadSuccess(active.name + " 설정 재로드 성공");
|
|
|
|
} catch (Exception e) {
|
|
logger.warn("HsmManager] KeyStore 재로드 실패:" + e.getMessage(), e);
|
|
logger.warn("HsmManager] 후보 설정(PRIMARY→SECONDARY) 전체 재연결을 시도합니다.");
|
|
reconnect(candidates);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 세션 누적, 네트워크 단절, 설정 변경 등으로 일반 재로드가 더 이상 복구되지 않을 때
|
|
* primary -> secondary 순서로 Provider 를 완전히 새로 생성한다.
|
|
*
|
|
* 신규 Provider/KeyStore 준비가 완전히 성공한 후에만 기존 Provider 를 제거하고 교체한다.
|
|
* 모든 후보가 실패하면 기존 Provider 와 keyStore 를 그대로 유지한다
|
|
* (서비스 중단보다 마지막 정상 상태 보존을 우선).
|
|
*/
|
|
private void reconnect(List<HsmConfig> candidates) throws Exception {
|
|
try {
|
|
HsmConnection connection = connectFirstAvailable(candidates);
|
|
applyConnection(connection);
|
|
recordReloadSuccess(connection.config.name + " 설정 재연결 성공");
|
|
logger.warn("HsmManager] HSM 재연결 성공. config=" + connection.config.name);
|
|
} catch (Exception e) {
|
|
recordReloadFailure(e.getMessage());
|
|
logger.warn("HsmManager] HSM 재연결 실패. 이전 keyStore 를 그대로 유지합니다: " + e.getMessage(), e);
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
private void recordReloadSuccess(String detail) {
|
|
lastReloadAt = System.currentTimeMillis();
|
|
lastReloadResult = "SUCCESS - " + detail;
|
|
reloadSuccessCount++;
|
|
}
|
|
|
|
private void recordReloadFailure(String detail) {
|
|
lastReloadAt = System.currentTimeMillis();
|
|
lastReloadResult = "FAIL - " + detail;
|
|
reloadFailCount++;
|
|
recordError(detail);
|
|
}
|
|
|
|
private void recordError(String message) {
|
|
lastErrorMessage = message;
|
|
lastErrorAt = System.currentTimeMillis();
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// PropManager 변경 즉시 반영
|
|
// -------------------------------------------------------------------------
|
|
|
|
private void registerPropertyChangeListener() {
|
|
if (propListenerRegistered) {
|
|
return;
|
|
}
|
|
try {
|
|
PropManager.getInstance().addPropertyChangeListener(this);
|
|
propListenerRegistered = true;
|
|
logger.warn("HsmManager] PropManager PropertyChangeListener 등록 완료");
|
|
} catch (Exception e) {
|
|
logger.warn("HsmManager] PropManager PropertyChangeListener 등록 실패(주기적 재로드로 대체): "
|
|
+ e.getMessage());
|
|
}
|
|
}
|
|
|
|
private void unregisterPropertyChangeListener() {
|
|
if (!propListenerRegistered) {
|
|
return;
|
|
}
|
|
try {
|
|
PropManager.getInstance().removePropertyChangeListener(this);
|
|
} catch (Exception e) {
|
|
logger.warn("HsmManager] PropManager PropertyChangeListener 해제 실패(무시): " + e.getMessage());
|
|
}
|
|
propListenerRegistered = false;
|
|
}
|
|
|
|
/**
|
|
* 관리 포털에서 PropManager.reload("HSM") 또는 setProperty 를 호출하면 수신된다.
|
|
* 재로드 자체는 HSM 통신을 수반하므로 호출 스레드를 막지 않도록 스케줄러 스레드에 위임한다.
|
|
*/
|
|
@Override
|
|
public void propertyChange(PropertyChangeEvent evt) {
|
|
if (!isHsmGroupEvent(evt)) {
|
|
return;
|
|
}
|
|
|
|
if (applyReloadInterval()) {
|
|
rescheduleReload();
|
|
}
|
|
|
|
ScheduledExecutorService currentScheduler = this.scheduler;
|
|
if (currentScheduler == null || currentScheduler.isShutdown()) {
|
|
return;
|
|
}
|
|
logger.warn("HsmManager] HSM 프로퍼티 변경 감지 → 설정 재적용을 요청합니다.");
|
|
currentScheduler.execute(this::reloadKeyStoreSafely);
|
|
}
|
|
|
|
/**
|
|
* PropManager 는 reload(group) 시 propertyName 에 그룹명을, setProperty 시 키명을 담고
|
|
* source 에는 항상 해당 그룹의 PropGroupVO 를 담는다. 두 경우를 모두 인식한다.
|
|
*/
|
|
private boolean isHsmGroupEvent(PropertyChangeEvent evt) {
|
|
if (evt == null) {
|
|
return false;
|
|
}
|
|
if (GROUP_NAME.equals(evt.getPropertyName())) {
|
|
return true;
|
|
}
|
|
Object source = evt.getSource();
|
|
return (source instanceof PropGroupVO) && GROUP_NAME.equals(((PropGroupVO) source).getName());
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Provider 생성
|
|
// -------------------------------------------------------------------------
|
|
|
|
/**
|
|
* 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);
|
|
|
|
unregisterPropertyChangeListener();
|
|
|
|
if (reloadFuture != null) {
|
|
reloadFuture.cancel(false);
|
|
}
|
|
if (scheduler != null) {
|
|
scheduler.shutdown();
|
|
}
|
|
|
|
if (pkcs11Provider != null) {
|
|
if (pkcs11Provider instanceof AuthProvider) {
|
|
try {
|
|
((AuthProvider) pkcs11Provider).logout();
|
|
} catch (Exception e) {
|
|
logger.warn("HsmManager] 종료 시 logout 실패(무시): " + e.getMessage());
|
|
}
|
|
}
|
|
Security.removeProvider(pkcs11Provider.getName());
|
|
}
|
|
pkcs11Provider = null;
|
|
keyStore = null;
|
|
activeConfig = null;
|
|
activeSince = 0;
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* isReady() 와 달리 실제 HSM 호출로 세션 생존 여부까지 확인하는 헬스체크.
|
|
* 모니터링/헬스체크 엔드포인트에서 사용을 권장한다.
|
|
*/
|
|
public boolean isHealthy() {
|
|
if (!isReady()) {
|
|
return false;
|
|
}
|
|
try {
|
|
logKeyStore(keyStore);
|
|
return true;
|
|
} catch (Exception e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// 모니터링용 조회 (HsmStatusController)
|
|
// -------------------------------------------------------------------------
|
|
|
|
/** 현재 연결에 사용 중인 설정 이름(PRIMARY/SECONDARY). 미연결이면 null. */
|
|
public String getActiveConfigName() {
|
|
HsmConfig config = this.activeConfig;
|
|
return (config == null) ? null : config.name;
|
|
}
|
|
|
|
/** 현재 secondary 설정으로 절체된 상태인지 여부. */
|
|
public boolean isUsingSecondaryConfig() {
|
|
return CONFIG_SECONDARY.equals(getActiveConfigName());
|
|
}
|
|
|
|
/** 현재 연결에 실제로 사용된 pkcs11.cfg 내용. PIN 은 포함되지 않는다. */
|
|
public String getActiveConfigContent() {
|
|
HsmConfig config = this.activeConfig;
|
|
return (config == null) ? null : config.configContent;
|
|
}
|
|
|
|
/**
|
|
* 현재 PropManager 값 기준으로 실제 적용될 pkcs11.cfg 내용을 후보별로 반환한다.
|
|
* PIN 은 포함하지 않는다. (설정이름 -> cfg 내용, primary 우선순)
|
|
*/
|
|
public java.util.Map<String, String> getResolvedConfigContents() {
|
|
java.util.Map<String, String> contents = new java.util.LinkedHashMap<>();
|
|
try {
|
|
for (HsmConfig config : loadConfigCandidates()) {
|
|
contents.put(config.name, config.configContent);
|
|
}
|
|
} catch (Exception e) {
|
|
logger.warn("HsmManager] 연결 후보 설정 조회 실패: " + e.getMessage());
|
|
}
|
|
return contents;
|
|
}
|
|
|
|
/** 현재 KeyStore 의 alias 목록. HSM 통신이 발생한다. */
|
|
public List<String> getKeyAliases() throws Exception {
|
|
KeyStore ks = this.keyStore;
|
|
if (ks == null) {
|
|
return Collections.emptyList();
|
|
}
|
|
List<String> aliases = new ArrayList<>();
|
|
java.util.Enumeration<String> e = ks.aliases();
|
|
while (e.hasMoreElements()) {
|
|
aliases.add(e.nextElement());
|
|
}
|
|
return aliases;
|
|
}
|
|
|
|
public String getProviderName() {
|
|
Provider provider = this.pkcs11Provider;
|
|
return (provider == null) ? null : provider.getName();
|
|
}
|
|
|
|
public long getReloadIntervalMinutes() {
|
|
return reloadIntervalMinutes;
|
|
}
|
|
|
|
/** 현재 설정으로 연결된 시각(epoch millis). 미연결이면 0. */
|
|
public long getActiveSince() {
|
|
return activeSince;
|
|
}
|
|
|
|
public long getLastReloadAt() {
|
|
return lastReloadAt;
|
|
}
|
|
|
|
public String getLastReloadResult() {
|
|
return lastReloadResult;
|
|
}
|
|
|
|
public String getLastErrorMessage() {
|
|
return lastErrorMessage;
|
|
}
|
|
|
|
public long getLastErrorAt() {
|
|
return lastErrorAt;
|
|
}
|
|
|
|
public int getReloadSuccessCount() {
|
|
return reloadSuccessCount;
|
|
}
|
|
|
|
public int getReloadFailCount() {
|
|
return reloadFailCount;
|
|
}
|
|
|
|
/** primary <-> secondary 절체가 발생한 횟수. */
|
|
public int getFailoverCount() {
|
|
return failoverCount;
|
|
}
|
|
}
|