diff --git a/src/test/java/com/eactive/eai/custom/adapter/http/dynamic/filter/CryptoFilterHsmIntegrationTest.java b/src/test/java/com/eactive/eai/custom/adapter/http/dynamic/filter/CryptoFilterHsmIntegrationTest.java deleted file mode 100644 index d385e06..0000000 --- a/src/test/java/com/eactive/eai/custom/adapter/http/dynamic/filter/CryptoFilterHsmIntegrationTest.java +++ /dev/null @@ -1,470 +0,0 @@ -package com.eactive.eai.custom.adapter.http.dynamic.filter; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assumptions.assumeTrue; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.io.File; -import java.io.InputStream; -import java.lang.reflect.Constructor; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Properties; -import java.util.UUID; -import java.util.concurrent.TimeUnit; - -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.MethodOrderer; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestMethodOrder; -import org.springframework.beans.factory.support.BeanDefinitionBuilder; -import org.springframework.context.support.GenericApplicationContext; - -import com.eactive.eai.adapter.http.dynamic.filter.InCryptoFilter; -import com.eactive.eai.adapter.http.filter.AbstractCryptoFilter; -import com.eactive.eai.common.hsm.HsmCryptoService; -import com.eactive.eai.common.hsm.HsmManager; -import com.eactive.eai.common.property.PropManager; -import com.eactive.eai.common.security.CryptoModuleConfigVO; -import com.eactive.eai.common.security.CryptoModuleManager; -import com.eactive.eai.common.security.CryptoModuleService; -import com.eactive.eai.common.security.keyderiv.strategy.HsmKeyDerivationStrategy; -import com.eactive.eai.common.security.loader.CryptoModuleConfigLoader; -import com.eactive.eai.common.security.mapper.CryptoModuleConfigMapper; -import com.eactive.eai.common.util.ApplicationContextProvider; -import com.eactive.eai.custom.common.security.keyderiv.HsmContextSha256KeyDerivationStrategy; -import com.eactive.eai.data.entity.onl.security.CryptoModuleConfig; - -/** - * CryptoFilter HSM 연동 통합 테스트 (SoftHSM2 / SunPKCS11) - * - * SoftHSM2에서 MASTER_KEY를 조회하여 키를 도출한 뒤 - * InCryptoFilter 의 암복호화 전체 흐름을 검증한다. - * - * 사전 조건: - * C:\SoftHSM2 에 SoftHSM2 설치 (미설치 시 자동 건너뜀) - * MASTER_KEY alias는 없으면 자동 생성됨 - * - * 검증 전략: - * HSM → 키 도출 → CryptoModuleExtension → InCryptoFilter doPostFilter(암호화) → doPreFilter(복호화) → 원문 일치 - */ -@TestMethodOrder(MethodOrderer.DisplayName.class) -public class CryptoFilterHsmIntegrationTest { - - // ------------------------------------------------------------------------- - // 상수 - // ------------------------------------------------------------------------- - - 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 MASTER_KEY_ALIAS = "MASTER_KEY"; - - /** HsmKeyDerivationStrategy: HSM 마스터키를 파생 없이 직접 사용 */ - private static final String MOD_HSM_GCM = "HSM_GCM"; - /** HsmContextSha256KeyDerivationStrategy: HSM 마스터키 + 컨텍스트값 SHA-256 파생 */ - private static final String MOD_CTX_SHA_GCM = "CTX_SHA256_GCM"; - - private static final String CTX_KEY = "X-Api-Group-Seq"; - private static final String CTX_VAL_A = "GROUP-A"; - private static final String CTX_VAL_B = "GROUP-B"; - - // ------------------------------------------------------------------------- - // 정적 필드 - // ------------------------------------------------------------------------- - - private static GenericApplicationContext springCtx; - private static HsmManager hsmManager; - private static File tempCfgFile; - - // ------------------------------------------------------------------------- - // 내부 클래스: runtimeContext를 고정값으로 반환하는 필터 - // ------------------------------------------------------------------------- - - static class ContextAwareCryptoFilter extends InCryptoFilter { - private final String key; - private final String value; - - ContextAwareCryptoFilter(String key, String value) { - this.key = key; - this.value = value; - } - - @Override - protected Map buildRuntimeContext(Properties prop, HttpServletRequest request) { - Map ctx = new HashMap<>(); - ctx.put(key, value); - return ctx; - } - } - - // ------------------------------------------------------------------------- - // 인스턴스 필드 - // ------------------------------------------------------------------------- - - private HttpServletRequest mockReq; - private HttpServletResponse mockRes; - - // ------------------------------------------------------------------------- - // 전체 설정 / 해제 - // ------------------------------------------------------------------------- - - @BeforeAll - static void setUpAll() throws Exception { - assumeTrue(new File(SOFTHSM2_DLL).exists(), - "SoftHSM2 미설치 (C:/SoftHSM2) - 테스트 건너뜀"); - System.setProperty("SOFTHSM2_CONF", "C:\\SoftHSM2\\etc\\softhsm2.conf"); - - // 1. 토큰 초기화 - ensureTokenInitialized(); - - // 2. PKCS11 설정 문자열 - // attributes(*,...): generate/import 모두 CKA_SENSITIVE=false → getEncoded() 사용 가능 - String cfgContent = "name = SoftHSM\n" - + "library = " + SOFTHSM2_DLL + "\n" - + "slotListIndex = 0\n" - + "attributes(*, CKO_SECRET_KEY, CKK_AES) = {\n" - + " CKA_SENSITIVE = false\n" - + " CKA_EXTRACTABLE = true\n" - + "}\n"; - tempCfgFile = File.createTempFile("pkcs11-filter-it-", ".cfg"); - Files.write(tempCfgFile.toPath(), cfgContent.getBytes("UTF-8")); - - // 3. Mock PropManager (DB 없이 HSM 설정값 제공) - PropManager mockPropManager = mock(PropManager.class); - when(mockPropManager.getProperty("HSM", "PKCS11_CONFIG")).thenReturn(cfgContent); - when(mockPropManager.getProperty("HSM", "PIN")).thenReturn(PIN); - when(mockPropManager.getProperty(eq("HSM"), eq("CACHE_RELOAD_YN"), anyString())).thenReturn("N"); - - // 4. HsmManager (private 생성자 → 리플렉션) - Constructor hsmCtor = HsmManager.class.getDeclaredConstructor(); - hsmCtor.setAccessible(true); - hsmManager = hsmCtor.newInstance(); - - // 5. CryptoModuleManager (private 생성자 → 리플렉션) - CryptoModuleConfigLoader mockLoader = mock(CryptoModuleConfigLoader.class); - when(mockLoader.findAll()).thenReturn(buildModuleConfigs()); - - Constructor managerCtor = CryptoModuleManager.class.getDeclaredConstructor(); - managerCtor.setAccessible(true); - CryptoModuleManager manager = managerCtor.newInstance(); - - CryptoModuleService cryptoService = new CryptoModuleService(); - HsmCryptoService hsmCryptoService = new HsmCryptoService(); - - // 6. Spring ApplicationContext 구성 - springCtx = new GenericApplicationContext(); - springCtx.getBeanFactory().registerSingleton("propManager", mockPropManager); - springCtx.getBeanFactory().registerSingleton("hsmManager", hsmManager); - springCtx.getBeanFactory().registerSingleton("hsmCryptoService", hsmCryptoService); - springCtx.getBeanFactory().registerSingleton("cryptoModuleConfigLoader", mockLoader); - springCtx.getBeanFactory().registerSingleton("cryptoModuleManager", manager); - springCtx.getBeanFactory().registerSingleton("cryptoModuleService", cryptoService); - springCtx.getBeanFactory().registerSingleton("cryptoModuleConfigMapper", testMapper()); - springCtx.registerBeanDefinition("applicationContextProvider", - BeanDefinitionBuilder.genericBeanDefinition(ApplicationContextProvider.class) - .getBeanDefinition()); - springCtx.refresh(); - - // 7. HsmManager 시작 (SunPKCS11 Provider 초기화 + KeyStore 오픈) - hsmManager.start(); - assertTrue(hsmManager.isReady(), "HsmManager 초기화 실패"); - System.out.println("[HSM] Provider: " + hsmManager.getPkcs11Provider().getName()); - - // 8. MASTER_KEY 등록 (없으면 자동 생성) - ensureMasterKey(); - - // 9. CryptoModuleManager 시작 (모듈 설정 로드) - manager.start(); - System.out.println("[Crypto] 모듈 로드 완료"); - } - - @AfterAll - static void tearDownAll() throws Exception { - if (hsmManager != null && hsmManager.isStarted()) hsmManager.stop(); - if (springCtx != null) springCtx.close(); - if (tempCfgFile != null) tempCfgFile.delete(); - } - - @BeforeEach - void setUp() { - mockReq = mock(HttpServletRequest.class); - mockRes = mock(HttpServletResponse.class); - } - - // ========================================================================= - // 1. HsmKeyDerivationStrategy — HSM 마스터키 직접 사용 (InCryptoFilter) - // ========================================================================= - - @Test - @DisplayName("1-1. FIELD/GCM HsmKey — doPostFilter 암호화 → doPreFilter 복호화 라운드트립") - void testHsmGcm_field_roundTrip() throws Exception { - InCryptoFilter filter = new InCryptoFilter(); - String original = "{\"acno\":\"1234567890\",\"amount\":\"100000\"}"; - - Properties prop = new Properties(); - prop.setProperty(AbstractCryptoFilter.PROP_MODULE_NAME, MOD_HSM_GCM); - // SCOPE 기본값 FIELD, 경로 기본값 사용 - - // 암호화: body 전체 → /encrypted_data 필드 - String encrypted = (String) filter.doPostFilter("G", "A", original, prop, mockReq, mockRes); - System.out.println("[1-1] encrypted: " + encrypted); - assertTrue(encrypted.contains("encrypted_data"), "/encrypted_data 필드가 있어야 한다"); - - // 복호화: /encrypted_data 필드 → body 전체 교체 - String decrypted = (String) filter.doPreFilter("G", "A", encrypted, prop, mockReq, mockRes); - System.out.println("[1-1] decrypted: " + decrypted); - assertEquals(original, decrypted); - } - - @Test - @DisplayName("1-2. BODY/GCM HsmKey — doPostFilter 암호화 → doPreFilter 복호화 라운드트립") - void testHsmGcm_body_roundTrip() throws Exception { - InCryptoFilter filter = new InCryptoFilter(); - String original = "{\"acno\":\"1234567890\",\"amount\":\"100000\"}"; - - Properties prop = new Properties(); - prop.setProperty(AbstractCryptoFilter.PROP_MODULE_NAME, MOD_HSM_GCM); - prop.setProperty(AbstractCryptoFilter.PROP_SCOPE, "BODY"); - - // 암호화: body 전체 → Base64 암호문 - String encrypted = (String) filter.doPostFilter("G", "A", original, prop, mockReq, mockRes); - System.out.println("[1-2] encrypted(Base64): " + encrypted); - assertNotEquals(original, encrypted, "암호문은 평문과 달라야 한다"); - - // 복호화: Base64 암호문 → 원문 - String decrypted = (String) filter.doPreFilter("G", "A", encrypted, prop, mockReq, mockRes); - System.out.println("[1-2] decrypted: " + decrypted); - assertEquals(original, decrypted); - } - - @Test - @DisplayName("1-3. FIELD/GCM HsmKey + AAD 헤더 — 동일 요청 ID로 암복호화 성공") - void testHsmGcm_field_withAad_success() throws Exception { - InCryptoFilter filter = new InCryptoFilter(); - String original = "{\"amount\":\"500000\"}"; - String requestId = "REQ-20240101-001"; - - when(mockReq.getHeader("X-Request-Id")).thenReturn(requestId); - - Properties prop = new Properties(); - prop.setProperty(AbstractCryptoFilter.PROP_MODULE_NAME, MOD_HSM_GCM); - prop.setProperty(AbstractCryptoFilter.PROP_AAD_HEADER, "X-Request-Id"); - - String encrypted = (String) filter.doPostFilter("G", "A", original, prop, mockReq, mockRes); - String decrypted = (String) filter.doPreFilter( "G", "A", encrypted, prop, mockReq, mockRes); - System.out.println("[1-3] decrypted: " + decrypted); - assertEquals(original, decrypted); - } - - @Test - @DisplayName("1-4. FIELD/GCM HsmKey + AAD 헤더 — 다른 요청 ID로 복호화 시 GCM 인증 실패") - void testHsmGcm_field_withAad_wrongAad_fails() throws Exception { - InCryptoFilter filter = new InCryptoFilter(); - String original = "{\"amount\":\"500000\"}"; - - // 암호화 시 요청 ID - HttpServletRequest encReq = mock(HttpServletRequest.class); - when(encReq.getHeader("X-Request-Id")).thenReturn("REQ-CORRECT"); - - // 복호화 시 다른 요청 ID - HttpServletRequest decReq = mock(HttpServletRequest.class); - when(decReq.getHeader("X-Request-Id")).thenReturn("REQ-WRONG"); - - Properties prop = new Properties(); - prop.setProperty(AbstractCryptoFilter.PROP_MODULE_NAME, MOD_HSM_GCM); - prop.setProperty(AbstractCryptoFilter.PROP_AAD_HEADER, "X-Request-Id"); - - String encrypted = (String) filter.doPostFilter("G", "A", original, prop, encReq, mockRes); - - assertThrows(Exception.class, - () -> filter.doPreFilter("G", "A", encrypted, prop, decReq, mockRes), - "잘못된 AAD로 GCM 복호화 시 예외가 발생해야 한다"); - System.out.println("[1-4] 다른 AAD 복호화 예외 확인 완료"); - } - - // ========================================================================= - // 2. HsmContextSha256KeyDerivationStrategy — 컨텍스트 기반 SHA-256 파생 키 - // ========================================================================= - - @Test - @DisplayName("2-1. FIELD/GCM ContextSha256 — 동일 컨텍스트로 암복호화 라운드트립") - void testCtxSha256Gcm_field_roundTrip() throws Exception { - InCryptoFilter filter = new ContextAwareCryptoFilter(CTX_KEY, CTX_VAL_A); - String original = "{\"accNo\":\"0011234567890\",\"amount\":\"50000\"}"; - - Properties prop = new Properties(); - prop.setProperty(AbstractCryptoFilter.PROP_MODULE_NAME, MOD_CTX_SHA_GCM); - - String encrypted = (String) filter.doPostFilter("G", "A", original, prop, mockReq, mockRes); - System.out.println("[2-1] encrypted: " + encrypted); - assertTrue(encrypted.contains("encrypted_data")); - - String decrypted = (String) filter.doPreFilter("G", "A", encrypted, prop, mockReq, mockRes); - System.out.println("[2-1] decrypted: " + decrypted); - assertEquals(original, decrypted); - } - - @Test - @DisplayName("2-2. FIELD/GCM ContextSha256 — 암호화와 다른 컨텍스트로 복호화 시 GCM 인증 실패") - void testCtxSha256Gcm_differentContext_throwsException() throws Exception { - InCryptoFilter encFilter = new ContextAwareCryptoFilter(CTX_KEY, CTX_VAL_A); - InCryptoFilter decFilter = new ContextAwareCryptoFilter(CTX_KEY, CTX_VAL_B); - String original = "{\"data\":\"sensitive\"}"; - - Properties prop = new Properties(); - prop.setProperty(AbstractCryptoFilter.PROP_MODULE_NAME, MOD_CTX_SHA_GCM); - - String encrypted = (String) encFilter.doPostFilter("G", "A", original, prop, mockReq, mockRes); - - assertThrows(Exception.class, - () -> decFilter.doPreFilter("G", "A", encrypted, prop, mockReq, mockRes), - "다른 컨텍스트 키로 파생된 키는 복호화에 실패해야 한다"); - System.out.println("[2-2] 다른 컨텍스트 복호화 예외 확인 완료"); - } - - @Test - @DisplayName("2-3. FIELD/GCM ContextSha256 — 컨텍스트별 독립 암복호화 (GROUP-A, GROUP-B 각각 성공)") - void testCtxSha256Gcm_perGroupRoundTrip() throws Exception { - InCryptoFilter filterA = new ContextAwareCryptoFilter(CTX_KEY, CTX_VAL_A); - InCryptoFilter filterB = new ContextAwareCryptoFilter(CTX_KEY, CTX_VAL_B); - String plainA = "{\"group\":\"A\",\"amount\":\"1000\"}"; - String plainB = "{\"group\":\"B\",\"amount\":\"2000\"}"; - - Properties prop = new Properties(); - prop.setProperty(AbstractCryptoFilter.PROP_MODULE_NAME, MOD_CTX_SHA_GCM); - - String encA = (String) filterA.doPostFilter("G", "A", plainA, prop, mockReq, mockRes); - String encB = (String) filterB.doPostFilter("G", "B", plainB, prop, mockReq, mockRes); - - assertEquals(plainA, (String) filterA.doPreFilter("G", "A", encA, prop, mockReq, mockRes)); - assertEquals(plainB, (String) filterB.doPreFilter("G", "B", encB, prop, mockReq, mockRes)); - System.out.println("[2-3] GROUP-A, GROUP-B 각각 독립 암복호화 성공"); - } - - // ========================================================================= - // 헬퍼 - // ========================================================================= - - private static List buildModuleConfigs() { - return Arrays.asList( - buildDynamic(MOD_HSM_GCM, "AES", "GCM", "NoPadding", - HsmKeyDerivationStrategy.class.getName(), - "{\"hsmKeyAlias\":\"" + MASTER_KEY_ALIAS + "\"}"), - buildDynamic(MOD_CTX_SHA_GCM, "AES", "GCM", "NoPadding", - HsmContextSha256KeyDerivationStrategy.class.getName(), - "{\"hsmKeyAlias\":\"" + MASTER_KEY_ALIAS + "\",\"contextKey\":\"" + CTX_KEY + "\"}") - ); - } - - private static CryptoModuleConfig buildDynamic(String name, String alg, String mode, String padding, - String strategy, String params) { - CryptoModuleConfig c = new CryptoModuleConfig(); - c.setCryptoId(UUID.randomUUID().toString()); - c.setCryptoName(name); - c.setAlgType(alg); - c.setCipherMode(mode); - c.setPadding(padding); - c.setKeySourceType("DYNAMIC"); - c.setKeyDerivStrategy(strategy); - c.setKeyDerivParams(params); - c.setCacheYn("Y"); - c.setCacheTtlSec(60); - c.setUseYn("Y"); - // ivHex = null: GCM에서 IV를 AAD 대체값으로 사용하지 않도록 의도적으로 미설정 - return c; - } - - private static void ensureMasterKey() throws Exception { - java.security.KeyStore ks = hsmManager.getKeyStore(); - - // 이전 실행 키 삭제 (sensitive 여부 무관하게 항상 고정키로 재등록) - if (ks.containsAlias(MASTER_KEY_ALIAS)) { - ks.deleteEntry(MASTER_KEY_ALIAS); - System.out.println("[HSM] 기존 MASTER_KEY 삭제"); - } - - // CryptoModuleServiceTest.KEY_128 과 동일한 고정 키 → 두 테스트 간 암호문 비교 가능 - // attributes(*, CKO_SECRET_KEY, CKK_AES) 지시어로 임포트 시 CKA_SENSITIVE=false 적용 - byte[] keyBytes = "0123456789abcdef".getBytes(StandardCharsets.UTF_8); - SecretKey fixedKey = new SecretKeySpec(keyBytes, "AES"); - ks.setKeyEntry(MASTER_KEY_ALIAS, fixedKey, null, null); - - // 임포트 후 getEncoded() 검증 - SecretKey stored = (SecretKey) ks.getKey(MASTER_KEY_ALIAS, null); - byte[] encoded = stored.getEncoded(); - System.out.println("[HSM] MASTER_KEY 등록 완료: alias=" + MASTER_KEY_ALIAS - + " / getEncoded()=" + (encoded != null ? encoded.length + "bytes, " + new String(encoded) : "null (추출 불가!)")); - } - - 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 CryptoModuleConfigMapper testMapper() { - return new CryptoModuleConfigMapper() { - @Override - public CryptoModuleConfigVO toVo(CryptoModuleConfig e) { - CryptoModuleConfigVO vo = new CryptoModuleConfigVO(); - vo.setCryptoId(e.getCryptoId()); - vo.setCryptoName(e.getCryptoName()); - vo.setCryptoDesc(e.getCryptoDesc()); - vo.setAlgType(e.getAlgType()); - vo.setCipherMode(e.getCipherMode()); - vo.setPadding(e.getPadding()); - vo.setIvHex(e.getIvHex()); - vo.setKeySourceType(e.getKeySourceType()); - vo.setEncKeyHex(e.getEncKeyHex()); - vo.setDecKeyHex(e.getDecKeyHex()); - vo.setKeyDerivStrategy(e.getKeyDerivStrategy()); - vo.setKeyDerivParams(e.getKeyDerivParams()); - vo.setCacheYn(e.getCacheYn()); - vo.setCacheTtlSec(e.getCacheTtlSec()); - vo.setUseYn(e.getUseYn()); - return vo; - } - - @Override - public CryptoModuleConfig toEntity(CryptoModuleConfigVO vo) { - return null; - } - }; - } - - 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(); - } -}