package at.gv.egiz.eaaf.core.impl.credential; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.security.Key; import java.security.KeyStore; import java.security.KeyStore.LoadStoreParameter; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.Provider; import java.security.Security; import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.security.spec.InvalidKeySpecException; import java.security.spec.KeySpec; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.annotation.PostConstruct; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.PBEKeySpec; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import at.gv.egiz.eaaf.core.api.idp.IConfiguration; import at.gv.egiz.eaaf.core.exception.EaafKeyAccessException; import at.gv.egiz.eaaf.core.exceptions.EaafConfigurationException; import at.gv.egiz.eaaf.core.exceptions.EaafException; import at.gv.egiz.eaaf.core.exceptions.EaafFactoryException; import at.gv.egiz.eaaf.core.impl.credential.KeyStoreConfiguration.KeyStoreType; import at.gv.egiz.eaaf.core.impl.credential.SymmetricKeyConfiguration.SymmetricKeyType; import at.gv.egiz.eaaf.core.impl.data.Pair; import at.gv.egiz.eaaf.core.impl.utils.FileUtils; import at.gv.egiz.eaaf.core.impl.utils.KeyStoreUtils; import lombok.extern.slf4j.Slf4j; @Slf4j public class EaafKeyStoreFactory { public static final String CONFIG_PROP_HSM_FACADE_HOST = "security.hsmfacade.host"; public static final String CONFIG_PROP_HSM_FACADE_PORT = "security.hsmfacade.port"; public static final String CONFIG_PROP_HSM_FACADE_SSLTRUST = "security.hsmfacade.trustedsslcert"; public static final String CONFIG_PROP_HSM_FACADE_CLIENT_USERNAME = "security.hsmfacade.username"; public static final String CONFIG_PROP_HSM_FACADE_CLIENT_PASSWORD = "security.hsmfacade.password"; public static final String CONFIG_PROP_HSM_FACADE_GRPC_DEADLINE = "security.hsmfacade.grpc.deadline"; public static final String ERRORCODE_00 = "internal.keystore.00"; public static final String ERRORCODE_01 = "internal.keystore.01"; public static final String ERRORCODE_02 = "internal.keystore.02"; public static final String ERRORCODE_03 = "internal.keystore.03"; public static final String ERRORCODE_04 = "internal.keystore.04"; public static final String ERRORCODE_05 = "internal.keystore.05"; public static final String ERRORCODE_06 = "internal.keystore.06"; public static final String ERRORCODE_07 = "internal.keystore.07"; public static final String ERRORCODE_10 = "internal.keystore.10"; public static final String ERRORCODE_11 = "internal.keystore.11"; public static final String ERRORCODE_KEY_00 = "internal.key.00"; private static final String HSM_FACADE_PROVIDER_CLASS = "at.asitplus.hsmfacade.provider.HsmFacadeProvider"; private static final String HSM_FACADE_KEYSTORELOADPARAMETERS_CLASS = "at.asitplus.hsmfacade.provider.RemoteKeyStoreLoadParameter"; private static final String HSM_FACADE_PROVIDER_METHOD_CONSTRUCT = "getInstance"; private static final String HSM_FACADE_PROVIDER_METHOD_INIT = "init"; private static final String HSM_FACADE_PROVIDER_METHOD_ISINITIALIZED = "isInitialized"; private static final String HSM_FACADE_PROVIDER_METHOD_HEALTHCHECK = "healthcheck"; private static final String HSM_FACADE_PROVIDER_INIT_ERROR_MSG = "Has HSM-Facade class supported '{}' method: {}"; private static final String HSM_FACADE_PROVIDER = "HsmFacade"; private static final String HSM_FACADE_KEYSTORE_TYPE = "RemoteKeyStore"; private static final String HSM_FACADE_DEFAULT_DEADLINE = "30"; public enum HsmFacadeStatus { UP, DOWN, UNKNOWN } @Autowired private IConfiguration basicConfig; @Autowired private ResourceLoader resourceLoader; private boolean isHsmFacadeInitialized = false; /** * Get a new symmetric key based on a {@link SymmetricKeyConfiguration} object. * * @param config Symmetric key configuration * @return {@link Pair} of a new {@link SecretKey} instance and an optional {@link Provider}. * The {@link SecretKey} is {@link Nonnull}. If the {@link Provider} is not null * this {@link SecretKey} requires a specific {@link Provider} for {@link Key} operations. * @throws EaafException In case of a KeyStore initialization error */ @Nonnull public Pair buildNewSymmetricKey(SymmetricKeyConfiguration config) throws EaafException { log.trace("Starting symmetric-key generation based on configuration object ... "); if (SymmetricKeyType.PASSPHRASE.equals(config.getKeyType())) { return generatePassPhraseBasedSymmetricKey(config); } else if (SymmetricKeyType.HSMFACADE.equals(config.getKeyType())) { if (isHsmFacadeInitialized) { return getSymmetricKeyFromHsmFacade(config); } else { log.error("HSMFacade can NOT be used for symmetric Key: {} because {} is not initialized", config.getFriendlyName()); throw new EaafConfigurationException(ERRORCODE_00, new Object[] { config.getFriendlyName() }); } } else { log.warn("Symmetric KeyType: {} is unrecognized", config.getKeyType()); throw new EaafConfigurationException(ERRORCODE_01, new Object[] { config.getFriendlyName() }); } } /** * Get a new KeyStore based on a KeyStore configuration-object. * * @param config KeyStore configuration * @return {@link Pair} of a new {@link KeyStore} instance and an optional {@link Provider}. * The {@link KeyStore} is {@link Nonnull}. If the {@link Provider} is not null * this {@link KeyStore} requires a specific {@link Provider} for {@link Key} operations. * @throws EaafException In case of a KeyStore initialization error */ @Nonnull public Pair buildNewKeyStore(KeyStoreConfiguration config) throws EaafException { log.trace("Starting KeyStore generation based on configuration object ... "); if (KeyStoreType.PKCS12.equals(config.getKeyStoreType()) || KeyStoreType.JKS.equals(config.getKeyStoreType())) { return getKeyStoreFromFileSystem(config); } else if (KeyStoreType.HSMFACADE.equals(config.getKeyStoreType())) { if (isHsmFacadeInitialized) { return getKeyStoreFromHsmFacade(config); } else { log.error("HSMFacade can NOT be used for KeyStore: {} because {} is not initialized", config.getFriendlyName()); throw new EaafConfigurationException(ERRORCODE_00, new Object[] { config.getFriendlyName() }); } } else if (KeyStoreType.PKCS11.equals(config.getKeyStoreType())) { log.warn("KeyStoreType: {} is NOT supported", config.getKeyStoreType()); throw new EaafConfigurationException(ERRORCODE_02, new Object[] { config.getFriendlyName(), config.getKeyStoreType() }); } else { log.warn("KeyStoreType: {} is unrecognized", config.getKeyStoreType()); throw new EaafConfigurationException(ERRORCODE_01, new Object[] { config.getFriendlyName() }); } } /** * Get the initialization state of the HSM Facade module. * * @return true if HSM Facade is available, otherwise false */ public boolean isHsmFacadeInitialized() { return isHsmFacadeInitialized; } /** * Get the current status for HSM-Facade interaction. * * @return {@link HsmFacadeStatus} to indicate the current status. */ public HsmFacadeStatus checkHsmFacadeStatus() { if (isHsmFacadeInitialized()) { final Provider alreadyLoadedProvider = Security.getProvider(HSM_FACADE_PROVIDER); if (alreadyLoadedProvider != null) { try { final Method healthCheck = alreadyLoadedProvider.getClass().getMethod(HSM_FACADE_PROVIDER_METHOD_HEALTHCHECK, new Class[]{}); boolean currentHealthStatus = (boolean) healthCheck.invoke(alreadyLoadedProvider); HsmFacadeStatus status = currentHealthStatus ? HsmFacadeStatus.UP : HsmFacadeStatus.DOWN; log.trace("Current HSM-Facade status is: {}", status); return status; } catch (final Exception e) { log.info("Can not determine state of alreay loaded HSM Facade: {} because HealthCheck not support", alreadyLoadedProvider.getVersion()); log.debug("Full HSM-Facade health-check exception", e); return HsmFacadeStatus.UNKNOWN; } } else { log.warn("HSM-Facade is marked as 'initialized', but not load as Security-Provider"); return HsmFacadeStatus.DOWN; } } else { log.trace("HSM-Facade is not initialized. Set status do 'unknown'"); return HsmFacadeStatus.UNKNOWN; } } @PostConstruct private void initialize() throws EaafException { final Class hsmProviderClazz = getHsmProviderClass(); if (hsmProviderClazz != null) { final String hsmFacadeHost = basicConfig.getBasicConfiguration(CONFIG_PROP_HSM_FACADE_HOST); final Provider alreadyLoadedProvider = Security.getProvider(HSM_FACADE_PROVIDER); if (alreadyLoadedProvider != null && alreadyLoadedProvider.getClass().isAssignableFrom(hsmProviderClazz)) { log.info("Find already initialized Java SecurityProvider: {}", alreadyLoadedProvider.getName()); //mark it as initialized if the state can not be determined boolean isAlreadyInitialized = true; try { final Method initializeCheck = alreadyLoadedProvider.getClass().getMethod(HSM_FACADE_PROVIDER_METHOD_ISINITIALIZED, new Class[]{}); isAlreadyInitialized = (boolean) initializeCheck.invoke(alreadyLoadedProvider); } catch (final Exception e) { log.warn("Can not determine state of alreay loaded HSM Facade. Mark it as 'initialized'"); log.debug("HSM Facade check error: {}", e.getMessage()); } isHsmFacadeInitialized = isAlreadyInitialized; if (isHsmFacadeInitialized) { log.info("HSM Facade is already initialized. {} can provide KeyStores based on remote HSM", EaafKeyStoreFactory.class.getSimpleName()); } else { log.info("HSM Facade is already loaded but not initialized. {} can NOT provide KeyStores based on remote HSM", EaafKeyStoreFactory.class.getSimpleName()); } } else if (StringUtils.isNotEmpty(hsmFacadeHost)) { log.debug("Find host for HSMFacade. Starting crypto provider initialization ... "); initializeHsmFacadeSecurityProvider(hsmProviderClazz, hsmFacadeHost); } else { log.info("HSM Facade is on ClassPath but not configurated. {} can only provide software keystores", EaafKeyStoreFactory.class.getSimpleName()); } } else { log.info("HSM Facade is not on ClassPath. {} can only provide software keystores", EaafKeyStoreFactory.class.getSimpleName()); } } private void initializeHsmFacadeSecurityProvider(Class hsmProviderClazz, String hsmFacadeHost) throws EaafException { try { final int port = Integer.parseUnsignedInt( getConfigurationParameter(CONFIG_PROP_HSM_FACADE_PORT)); final String clientUsername = getConfigurationParameter(CONFIG_PROP_HSM_FACADE_CLIENT_USERNAME); final String clientPassword = getConfigurationParameter(CONFIG_PROP_HSM_FACADE_CLIENT_PASSWORD); final long grpcDeadline = getConfigurationParameterLong(CONFIG_PROP_HSM_FACADE_GRPC_DEADLINE, HSM_FACADE_DEFAULT_DEADLINE); //initialize HSM-Facade by using JAVA Reflection, because in that case HSM-Facade //has not be in ClassPath on every project final Method constructor = hsmProviderClazz.getMethod(HSM_FACADE_PROVIDER_METHOD_CONSTRUCT, new Class[]{}); final Method initMethod = hsmProviderClazz.getMethod(HSM_FACADE_PROVIDER_METHOD_INIT, X509Certificate.class, String.class, String.class, String.class, int.class, long.class); if (initMethod != null && constructor != null) { final Object rawProvider = constructor.invoke(hsmProviderClazz); initMethod.invoke( rawProvider, getHsmFacadeTrustSslCertificate(), clientUsername, clientPassword, hsmFacadeHost, port, grpcDeadline); if (rawProvider instanceof Provider) { Security.addProvider((Provider) rawProvider); isHsmFacadeInitialized = true; log.info("HSM Facade is initialized. {} can provide KeyStores based on remote HSM", EaafKeyStoreFactory.class.getSimpleName()); } else { log.warn("Is HSM-Facade class type of 'java.security.Provider': {}", rawProvider instanceof Provider); throw new EaafException(ERRORCODE_10, new Object[] {HSM_FACADE_PROVIDER_CLASS}); } } else { log.warn(HSM_FACADE_PROVIDER_INIT_ERROR_MSG, HSM_FACADE_PROVIDER_METHOD_CONSTRUCT, constructor != null); log.warn(HSM_FACADE_PROVIDER_INIT_ERROR_MSG, HSM_FACADE_PROVIDER_METHOD_INIT, initMethod != null); throw new EaafException(ERRORCODE_10, new Object[] {HSM_FACADE_PROVIDER_CLASS}); } //final HsmFacadeProvider provider = HsmFacadeProvider.Companion.getInstance(); //provider.init(getHsmFacadeTrustSslCertificate(), clientUsername, clientPassword, hsmFacadeHost, port); } catch (final EaafException e) { throw e; } catch (final Exception e) { log.error("HSM Facade initialization FAILED with an generic error.", e); throw new EaafConfigurationException(ERRORCODE_03, new Object[] { e.getMessage() }, e); } } private Class getHsmProviderClass() { try { return Class.forName(HSM_FACADE_PROVIDER_CLASS); } catch (final ClassNotFoundException e1) { log.debug("No HSM-Facade implemenation in ClassPath. HSM-Facade will not be available"); return null; } } @Nonnull private Pair getKeyStoreFromFileSystem(KeyStoreConfiguration config) throws EaafConfigurationException, EaafFactoryException { try { final String keyStorePath = checkConfigurationParameter(config.getSoftKeyStoreFilePath(), ERRORCODE_06, config.getFriendlyName(), "Software-KeyStore missing filepath to KeyStore"); final String keyStorePassword = checkConfigurationParameter(config.getSoftKeyStorePassword(), ERRORCODE_06, config.getFriendlyName(), "Software-KeyStore missing Password for KeyStore"); Resource ressource; if (config.isSkipMakeAbsolutPaths()) { log.debug("Use filepath from config: {}", keyStorePath); ressource = resourceLoader.getResource(keyStorePath); } else { final String absKeyStorePath = FileUtils.makeAbsoluteUrl(keyStorePath, basicConfig .getConfigurationRootDirectory()); log.debug("Use filepath from config: {}", absKeyStorePath); ressource = resourceLoader.getResource(absKeyStorePath); } if (!ressource.exists()) { throw new EaafConfigurationException(ERRORCODE_06, new Object[] { config.getFriendlyName(), "RessourceLoader does NOT find File at: " + ressource.getURI() }); } final InputStream is = ressource.getInputStream(); final KeyStore keyStore = KeyStoreUtils.loadKeyStore(is, keyStorePassword, config.getKeyStoreType()); is.close(); return Pair.newInstance(keyStore, null); } catch (final EaafException e) { throw e; } catch (final IOException e) { throw new EaafFactoryException(ERRORCODE_06, new Object[] { config.getFriendlyName(), "KeyStore not valid or password wrong" }); } catch (final Exception e) { log.error("Software KeyStore initialization FAILED with an generic error.", e); throw new EaafConfigurationException(ERRORCODE_03, new Object[] { e.getMessage() }, e); } } @Nonnull private Pair getKeyStoreFromHsmFacade(KeyStoreConfiguration config) throws EaafFactoryException, EaafConfigurationException { return getKeyStoreFromHsmFacade(config.getKeyStoreName(), config.getFriendlyName()); } @Nonnull private Pair getKeyStoreFromHsmFacade(String keyStoreName, String friendlyName) throws EaafFactoryException, EaafConfigurationException { final String validatedKeyStoreName = checkConfigurationParameter(keyStoreName, ERRORCODE_06, friendlyName, "KeyStoreName missing for HSM Fac)ade"); try { final KeyStore keyStore = KeyStore.getInstance(HSM_FACADE_KEYSTORE_TYPE, HSM_FACADE_PROVIDER); keyStore.load(getHsmFacadeKeyStoreParameter(validatedKeyStoreName)); return Pair.newInstance(keyStore, keyStore.getProvider()); } catch (NoSuchAlgorithmException | CertificateException | IOException | KeyStoreException | NoSuchProviderException | EaafException e) { log.error("Can not initialize KeyStore: {} with reason: {}", friendlyName, e.getMessage()); throw new EaafFactoryException(ERRORCODE_06, new Object[] {friendlyName, e.getMessage() }, e); } } private KeyStore.LoadStoreParameter getHsmFacadeKeyStoreParameter(String keyStoreName) throws EaafException { try { final Class clazz = Class.forName(HSM_FACADE_KEYSTORELOADPARAMETERS_CLASS); final Constructor constructor = clazz.getConstructor(String.class); final Object keyStoreParams = constructor.newInstance(keyStoreName); return (LoadStoreParameter) keyStoreParams; } catch (final Exception e) { log.error("Can NOT build class: {} for HSM-Facade provider", HSM_FACADE_KEYSTORELOADPARAMETERS_CLASS, e); throw new EaafException(ERRORCODE_10, new Object[] {HSM_FACADE_PROVIDER_CLASS}, e); } } @Nonnull private Pair generatePassPhraseBasedSymmetricKey(SymmetricKeyConfiguration config) throws EaafConfigurationException { checkConfigurationParameter(config.getSoftKeyPassphrase(), ERRORCODE_KEY_00, config.getFriendlyName(), "passphrase missing"); checkConfigurationParameter(config.getSoftKeySalt(), ERRORCODE_KEY_00, config.getFriendlyName(), "salt missing"); try { final SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBKDF2WITHHMACSHA256"); final KeySpec spec = new PBEKeySpec( config.getSoftKeyPassphrase().toCharArray(), config.getSoftKeySalt().getBytes("UTF-8"), 10000, 128); return Pair.newInstance(keyFactory.generateSecret(spec), null); } catch (NoSuchAlgorithmException | InvalidKeySpecException | UnsupportedEncodingException e) { log.error("Passphrase based symmetric-key generation FAILED", e); throw new EaafConfigurationException(ERRORCODE_KEY_00, new Object[] { config.getFriendlyName(), e.getMessage() }, e); } } @Nonnull private Pair getSymmetricKeyFromHsmFacade(SymmetricKeyConfiguration config) throws EaafFactoryException, EaafConfigurationException, EaafKeyAccessException { final Pair keyStore = getKeyStoreFromHsmFacade( config.getKeyStoreName(), config.getFriendlyName()); checkConfigurationParameter(config.getKeyAlias(), ERRORCODE_KEY_00, config.getFriendlyName(), "keyAlias missing"); try { final SecretKey secretKey = (SecretKey) keyStore.getFirst().getKey(config.getKeyAlias(), null); if (secretKey == null) { throw new EaafKeyAccessException(EaafKeyAccessException.ERROR_CODE_09, config.getFriendlyName(), config.getKeyAlias(), "No SecretKey with Alias "); } return Pair.newInstance(secretKey, keyStore.getSecond()); } catch (UnrecoverableKeyException | KeyStoreException | NoSuchAlgorithmException e) { throw new EaafKeyAccessException(EaafKeyAccessException.ERROR_CODE_09, e, config.getFriendlyName(), config.getKeyAlias(), e.getMessage()); } catch (final ClassCastException e) { throw new EaafKeyAccessException(EaafKeyAccessException.ERROR_CODE_09, config.getFriendlyName(), config.getKeyAlias(), "Wrong SecretKey type "); } } private X509Certificate getHsmFacadeTrustSslCertificate() throws EaafConfigurationException { try { final String certFilePath = getConfigurationParameter(CONFIG_PROP_HSM_FACADE_SSLTRUST); final String absolutCertFilePath = FileUtils.makeAbsoluteUrl( certFilePath, basicConfig.getConfigurationRootDirectory()); final Resource certFile = resourceLoader.getResource(absolutCertFilePath); if (!certFile.exists()) { throw new EaafConfigurationException(ERRORCODE_05, new Object[] { CONFIG_PROP_HSM_FACADE_SSLTRUST, "File not found at: " + absolutCertFilePath }); } return (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(certFile .getInputStream()); } catch (final EaafConfigurationException e) { throw e; } catch (CertificateException | IOException e) { log.error("Can not load trusted server-certificate for HSM-Facade. Reason: {}", e.getMessage()); throw new EaafConfigurationException(ERRORCODE_05, new Object[] { CONFIG_PROP_HSM_FACADE_SSLTRUST, e.getMessage() }, e); } } @Nonnull private Long getConfigurationParameterLong(@Nonnull String configParamKey, String defaultValue) throws EaafConfigurationException { try { return Long.valueOf(basicConfig.getBasicConfiguration(configParamKey, defaultValue)); } catch (NumberFormatException e) { throw new EaafConfigurationException(ERRORCODE_05, new Object[] { configParamKey, e.getMessage()}); } } @Nonnull private String getConfigurationParameter(@Nonnull String configParamKey) throws EaafConfigurationException { return checkConfigurationParameter( basicConfig.getBasicConfiguration(configParamKey), ERRORCODE_04, configParamKey); } @Nonnull private String checkConfigurationParameter(@Nullable String configParam, @Nonnull String errorCode, @Nonnull String... errorParams) throws EaafConfigurationException { if (StringUtils.isEmpty(configParam)) { throw new EaafConfigurationException(errorCode, new Object[] { errorParams }); } return configParam; } }