From e2e77ed55687cb92c6f5a273995daf64dedef848 Mon Sep 17 00:00:00 2001 From: Christof Rabensteiner Date: Wed, 26 Jun 2019 08:47:58 +0200 Subject: Protect MsgClient via SSL (ink Client Authentication) - Add Component to create SSLContexts with own Key- and trust store. - Inject SSLContext into HTTP Client. - Add EAAF-Components Core Dependency, which is needed by SSLContextCreator (KeyStoreUtils). Schema Changes in mzs:DeliveryRequest/Config: - Got Rid of mzs:DeliveryRequest/Config/Server. In mzs 1.4.1, Server replaces the result of zkopf query person request. Since this zkopf interface does not exist anymore, Server was removed. - Add ClientType, which holds all parameters needed to connect to a service (Url, SSL params, a.o.). Configuration: - Add default parameters for SSL Clients in application.yaml. - Merge default parameters into incoming mzs:DeliveryRequests. MoaZSException Fixes: - Remove "Extends throwable" from Builder. - Add convenient shorthand init method (message, throwable). Refactor: - Put "determinePath" to FileUtils. - Put string related utility functions into StringUtils. --- pom.xml | 5 + readme.md | 4 +- src/main/java/at/gv/egiz/moazs/MoaZSException.java | 6 +- .../java/at/gv/egiz/moazs/config/MoaSigConfig.java | 19 +-- src/main/java/at/gv/egiz/moazs/msg/MsgClient.java | 64 +++++-- .../at/gv/egiz/moazs/msg/MsgClientFactory.java | 51 +++++- .../moazs/pipeline/SameThreadDeliveryPipeline.java | 4 +- .../moazs/preprocess/ConfigProfileGenerator.java | 19 +-- .../at/gv/egiz/moazs/preprocess/ConfigUtil.java | 183 ++++++++++++++++++--- .../moazs/preprocess/DeliveryRequestAugmenter.java | 6 +- src/main/java/at/gv/egiz/moazs/util/FileUtils.java | 22 +++ .../at/gv/egiz/moazs/util/SSLContextCreator.java | 82 +++++++++ .../java/at/gv/egiz/moazs/util/StringUtils.java | 17 ++ src/main/resources/application.yaml | 29 ++-- src/main/resources/mzs/app2mzs.xsd | 34 +++- .../egiz/moazs/DeliveryRequestAugmenterTest.java | 29 +++- src/test/java/at/gv/egiz/moazs/MsgClientTest.java | 33 +--- .../egiz/moazs/SameThreadDeliveryPipelineTest.java | 15 +- 18 files changed, 487 insertions(+), 135 deletions(-) create mode 100644 src/main/java/at/gv/egiz/moazs/util/FileUtils.java create mode 100644 src/main/java/at/gv/egiz/moazs/util/SSLContextCreator.java create mode 100644 src/main/java/at/gv/egiz/moazs/util/StringUtils.java diff --git a/pom.xml b/pom.xml index 85b7075..c27e764 100644 --- a/pom.xml +++ b/pom.xml @@ -124,6 +124,11 @@ eaaf_module_moa-sig ${eaaf-components.version} + + at.gv.egiz.eaaf + eaaf-core + ${eaaf-components.version} + diff --git a/readme.md b/readme.md index cd155bb..606dcf0 100644 --- a/readme.md +++ b/readme.md @@ -4,14 +4,14 @@ For compiling MOAZS, do the following: 1. Checkout MoaZS repository (directory name `moazs.git`) -2. Install `at.gv.egiz.eaaf.eaaf_module_moa-sig` to local repository. +2. Install `at.gv.egiz.eaaf.{eaaf_module_moa-sig, core}` to local repository. 3. Install `at.gv.util.egovutils` to local mvn repository. 4. Make `eaaf_module_moa-sig`'s transitive dependencies available by copying or symlinking `eaaf_module_moa-sig`'s repository into MOAZS repository. Certain artifacts are not in the mvn central repo. You need to install those articats to your local maven repository. -### How to Install `at.gv.egiz.eaaf.eaaf_module_moa-sig` +### How to Install `at.gv.egiz.eaaf` Artifacts Requirements: JDK 1.8 diff --git a/src/main/java/at/gv/egiz/moazs/MoaZSException.java b/src/main/java/at/gv/egiz/moazs/MoaZSException.java index db04241..847d9c1 100644 --- a/src/main/java/at/gv/egiz/moazs/MoaZSException.java +++ b/src/main/java/at/gv/egiz/moazs/MoaZSException.java @@ -32,6 +32,10 @@ public class MoaZSException extends RuntimeException { this.mzsRequest = mzsRequest; } + public static MoaZSException moaZSException(String message, Throwable cause) { + return moaZSExceptionBuilder(message).withCause(cause).build(); + } + public static MoaZSException moaZSException(String formatString, Object... objects) { return moaZSExceptionBuilder(formatString, objects).build(); } @@ -73,7 +77,7 @@ public class MoaZSException extends RuntimeException { return msgRequest; } - public static class Builder extends Throwable { + public static class Builder { private String message; private Throwable cause; diff --git a/src/main/java/at/gv/egiz/moazs/config/MoaSigConfig.java b/src/main/java/at/gv/egiz/moazs/config/MoaSigConfig.java index 05ecac1..0b7bdc7 100644 --- a/src/main/java/at/gv/egiz/moazs/config/MoaSigConfig.java +++ b/src/main/java/at/gv/egiz/moazs/config/MoaSigConfig.java @@ -2,10 +2,12 @@ package at.gv.egiz.moazs.config; import at.gv.egiz.eid.authhandler.modules.sigverify.moasig.api.ISignatureVerificationService; import at.gv.egiz.eid.authhandler.modules.sigverify.moasig.impl.SignatureVerificationService; +import at.gv.egiz.moazs.util.FileUtils; import at.gv.egiz.moazs.verify.MoaSPSSSignatureVerifier; import at.gv.egiz.moazs.verify.SignatureVerifier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -28,15 +30,18 @@ public class MoaSigConfig { private final String keyStoreType; private final String defaultTrustProfile; private final String spssConfigFilePath; + private final FileUtils fileUtils; public MoaSigConfig(@Value("${javax.net.ssl.trustStoreType}") String trustStoreType, @Value("${javax.net.ssl.keyStoreType}") String keyStoreType, @Value("${moa.spss.server.default-trustprofile}") String defaultTrustProfile, - @Value("${moa.spss.server.configuration}") String spssConfigFilePath) throws FileNotFoundException { + @Value("${moa.spss.server.configuration}") String spssConfigFilePath, + @Autowired FileUtils fileUtils) throws FileNotFoundException { this.trustStoreType = trustStoreType; this.keyStoreType = keyStoreType; this.defaultTrustProfile = defaultTrustProfile; this.spssConfigFilePath = spssConfigFilePath; + this.fileUtils = fileUtils; fallBackToSpringEnvForMoaSPSSConfigProperty(); fallBackToSpringEnvForJavaxNetSSLStoreTypeProperty(); } @@ -45,7 +50,7 @@ public class MoaSigConfig { log.info("value of spssConfigFilePath is {}", spssConfigFilePath); if(System.getProperty(MOA_SPSS_CONFIG_FILE_PROPERTY) == null) { - var realPath = determinePath(spssConfigFilePath); + var realPath = fileUtils.determinePath(spssConfigFilePath); var realFile = new File(realPath); if(realFile.exists() && realFile.canRead()) { @@ -57,15 +62,7 @@ public class MoaSigConfig { } } - private String determinePath(String abstractPath) { - if (new File(abstractPath).isAbsolute()) { - return abstractPath; - } else { - //resolve relative path as classpath resource - //java.lang.Class needs relative resources to start with "/" - return this.getClass().getResource("/" + abstractPath).getFile(); - } - } + private void fallBackToSpringEnvForJavaxNetSSLStoreTypeProperty() { if (System.getProperty(JAVAX_SSL_TRUSTSTORE_TYPE_PROPERTY) == null) { diff --git a/src/main/java/at/gv/egiz/moazs/msg/MsgClient.java b/src/main/java/at/gv/egiz/moazs/msg/MsgClient.java index 82f172d..84a7801 100644 --- a/src/main/java/at/gv/egiz/moazs/msg/MsgClient.java +++ b/src/main/java/at/gv/egiz/moazs/msg/MsgClient.java @@ -1,46 +1,80 @@ package at.gv.egiz.moazs.msg; -import at.gv.zustellung.app2mzs.xsd.ConfigType; import at.gv.zustellung.msg.xsd.App2ZusePort; +import at.gv.zustellung.msg.xsd.App2ZusePortService; import at.gv.zustellung.msg.xsd.DeliveryRequestStatusType; import at.gv.zustellung.msg.xsd.DeliveryRequestType; +import org.apache.cxf.configuration.jsse.TLSClientParameters; +import org.apache.cxf.endpoint.Client; +import org.apache.cxf.frontend.ClientProxy; import org.apache.cxf.jaxws.JaxWsClientFactoryBean; import org.apache.cxf.jaxws.JaxWsProxyFactoryBean; import org.apache.cxf.message.Message; import org.apache.cxf.phase.PhaseInterceptor; +import org.apache.cxf.transport.http.HTTPConduit; +import org.apache.cxf.transports.http.configuration.HTTPClientPolicy; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.lang.Nullable; +import javax.net.ssl.SSLContext; + +//TODO: Rethink design. could this entire class be replaced? +// Because everything the send() method does could be initialized in +// the MsgClientFactory as well. public class MsgClient { - private final DeliveryRequestType msgRequest; - private final ConfigType config; + private static final Logger log = LoggerFactory.getLogger(MsgClient.class); + private final PhaseInterceptor interceptor; - public MsgClient(DeliveryRequestType msgRequest, ConfigType config, PhaseInterceptor interceptor) { - this.msgRequest = msgRequest; - this.config = config; + private final String address; + + //TODO: make configurable + private final int connectionTimeout = 60; + private final int receiveTimeout = 60; + + @Nullable + private final SSLContext sslContext; + + public MsgClient(PhaseInterceptor interceptor, + String address, + @Nullable SSLContext sslContext) { this.interceptor = interceptor; + this.address = address; + this.sslContext = sslContext; } /** * Send {@code msgRequest} to {@code Config/Server/ZUSEUrlID} and run {@code interceptor} on response. * @return */ - public DeliveryRequestStatusType send() { - var proxy = connect(config); - return proxy.delivery(msgRequest); - } - - private App2ZusePort connect(ConfigType config) { - - var address = config.getServer().getZUSEUrlID(); + public DeliveryRequestStatusType send(DeliveryRequestType msgRequest) { var factory = new JaxWsClientFactoryBean(); + factory.setServiceClass(App2ZusePort.class); factory.setAddress(address); factory.getInInterceptors().add(interceptor); var proxy = new JaxWsProxyFactoryBean(factory).create(); - return (App2ZusePort) proxy; + Client client = ClientProxy.getClient(proxy); + HTTPConduit http = (HTTPConduit) client.getConduit(); + + var httpClientPolicy = new HTTPClientPolicy(); + httpClientPolicy.setConnectionTimeout(connectionTimeout); + httpClientPolicy.setReceiveTimeout(receiveTimeout); + http.setClient(httpClientPolicy); + + if (sslContext != null) { + var tlsParams = new TLSClientParameters(); + tlsParams.setSSLSocketFactory(sslContext.getSocketFactory()); + http.setTlsClientParameters(tlsParams); + log.info("SSLContext initialized. "); + } + + return ((App2ZusePort)proxy).delivery(msgRequest); } + } diff --git a/src/main/java/at/gv/egiz/moazs/msg/MsgClientFactory.java b/src/main/java/at/gv/egiz/moazs/msg/MsgClientFactory.java index c2cf34f..d4cc9f1 100644 --- a/src/main/java/at/gv/egiz/moazs/msg/MsgClientFactory.java +++ b/src/main/java/at/gv/egiz/moazs/msg/MsgClientFactory.java @@ -1,14 +1,57 @@ package at.gv.egiz.moazs.msg; -import at.gv.zustellung.app2mzs.xsd.ConfigType; -import at.gv.zustellung.msg.xsd.DeliveryRequestType; +import at.gv.egiz.moazs.util.FileUtils; +import at.gv.egiz.moazs.util.SSLContextCreator; +import at.gv.zustellung.app2mzs.xsd.ClientType; +import at.gv.zustellung.app2mzs.xsd.KeyStoreType; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import javax.net.ssl.SSLContext; + +import static at.gv.zustellung.app2mzs.xsd.KeyStoreType.keyStoreTypeBuilder; + @Component public class MsgClientFactory { - public MsgClient create(DeliveryRequestType msgRequest, ConfigType config, StoreSOAPBodyBinaryInRepositoryInterceptor storeResponseInterceptor) { - return new MsgClient(msgRequest, config, storeResponseInterceptor); + private final StoreSOAPBodyBinaryInRepositoryInterceptor storeResponseInterceptor; + private final SSLContextCreator sslContextCreator; + private final FileUtils fileUtils; + + + @Autowired + public MsgClientFactory(StoreSOAPBodyBinaryInRepositoryInterceptor storeResponseInterceptor, SSLContextCreator creator, FileUtils fileUtils) { + this.storeResponseInterceptor = storeResponseInterceptor; + this.sslContextCreator = creator; + this.fileUtils = fileUtils; + } + + + /** + * Creates a client that communicates with a msg service. + * + * @param params for the client, such as service url and ssl parameters. + * @return the msg client + */ + //TODO evaluate and honor laxhostnameverification and trustall parameter! + public MsgClient create(ClientType params) { + + SSLContext sslContext = null; + + if (params.getURL().startsWith("https")) { + var keystore = resolveKeyStorePath(params.getSSL().getKeyStore()); + var truststore = resolveKeyStorePath(params.getSSL().getTrustStore()); + sslContext = sslContextCreator.createSSLContext(keystore, truststore); + } + + return new MsgClient(storeResponseInterceptor, params.getURL(), sslContext); + } + + private KeyStoreType resolveKeyStorePath(KeyStoreType store) { + return store == null ? null + : keyStoreTypeBuilder(store) + .withFileName(fileUtils.determinePath(store.getFileName())) + .build(); } } diff --git a/src/main/java/at/gv/egiz/moazs/pipeline/SameThreadDeliveryPipeline.java b/src/main/java/at/gv/egiz/moazs/pipeline/SameThreadDeliveryPipeline.java index ae8286f..20320c4 100644 --- a/src/main/java/at/gv/egiz/moazs/pipeline/SameThreadDeliveryPipeline.java +++ b/src/main/java/at/gv/egiz/moazs/pipeline/SameThreadDeliveryPipeline.java @@ -80,7 +80,7 @@ public class SameThreadDeliveryPipeline implements DeliveryPipeline { exceptionBuilder.withMsgRequest(msgRequest); - var status = msgClientFactory.create(msgRequest, mzsRequest.getConfig(), interceptor).send(); + var status = msgClientFactory.create(mzsRequest.getConfig().getMSGClient()).send(msgRequest); exceptionBuilder.withMsgResult(status); verifySignedStatus(appDeliveryId, exceptionBuilder); @@ -128,7 +128,7 @@ public class SameThreadDeliveryPipeline implements DeliveryPipeline { .withAppDeliveryID(appDeliveryId); if (exception.getMzsRequest() != null) { - errorBuilder.withDeliverySystem(exception.getMzsRequest().getConfig().getServer().getZUSEUrlID()); + errorBuilder.withDeliverySystem(exception.getMzsRequest().getConfig().getMSGClient().getURL()); } if (exception.getTnvzResult() != null && exception.getTnvzResult().getError() != null) { diff --git a/src/main/java/at/gv/egiz/moazs/preprocess/ConfigProfileGenerator.java b/src/main/java/at/gv/egiz/moazs/preprocess/ConfigProfileGenerator.java index be14852..fa1ccd6 100644 --- a/src/main/java/at/gv/egiz/moazs/preprocess/ConfigProfileGenerator.java +++ b/src/main/java/at/gv/egiz/moazs/preprocess/ConfigProfileGenerator.java @@ -1,6 +1,7 @@ package at.gv.egiz.moazs.preprocess; import at.gv.egiz.moazs.MoaZSException; +import at.gv.egiz.moazs.util.StringUtils; import at.gv.zustellung.app2mzs.xsd.ConfigType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -58,9 +59,9 @@ public class ConfigProfileGenerator { var groupedKeys = properties.getPropertyNames() .filter(this::isConfigurationProfileProperty) - .map(this::removePrefix) - .filter(this::hasPrefix) - .collect(groupingBy(this::keepPrefix, mapping(this::removePrefix, toSet()))); + .map(StringUtils::removePrefix) + .filter(StringUtils::hasPrefix) + .collect(groupingBy(StringUtils::keepPrefix, mapping(StringUtils::removePrefix, toSet()))); var profiles = groupedKeys.entrySet().stream() .collect(toUnmodifiableMap(Entry::getKey, this::createConfigFromEnv)); @@ -78,22 +79,10 @@ public class ConfigProfileGenerator { return defaultProfile == null ? profiles : mergeProfiles(profiles, defaultProfile); } - private boolean hasPrefix(String name) { - return name.indexOf('.') != -1; - } - private boolean isConfigurationProfileProperty(String propName) { return propName.startsWith(profilePrefix + "."); } - private String keepPrefix(String name) { - return name.substring(0, name.indexOf('.')); - } - - private String removePrefix(String name) { - return name.substring(name.indexOf('.') + 1); - } - private ConfigType createConfigFromEnv(Entry> entry) { var profile = entry.getKey(); diff --git a/src/main/java/at/gv/egiz/moazs/preprocess/ConfigUtil.java b/src/main/java/at/gv/egiz/moazs/preprocess/ConfigUtil.java index 3fef4bd..1befd1d 100644 --- a/src/main/java/at/gv/egiz/moazs/preprocess/ConfigUtil.java +++ b/src/main/java/at/gv/egiz/moazs/preprocess/ConfigUtil.java @@ -1,20 +1,37 @@ package at.gv.egiz.moazs.preprocess; +import at.gv.egiz.moazs.util.StringUtils; +import at.gv.zustellung.app2mzs.xsd.ClientType; import at.gv.zustellung.app2mzs.xsd.ConfigType; -import at.gv.zustellung.app2mzs.xsd.ServerType; +import at.gv.zustellung.app2mzs.xsd.KeyStoreType; +import at.gv.zustellung.app2mzs.xsd.SSLType; import org.springframework.lang.Nullable; import org.springframework.stereotype.Component; import java.util.Map; +import static at.gv.zustellung.app2mzs.xsd.ClientType.clientTypeBuilder; import static at.gv.zustellung.app2mzs.xsd.ConfigType.configTypeBuilder; -import static at.gv.zustellung.app2mzs.xsd.ServerType.serverTypeBuilder; +import static at.gv.zustellung.app2mzs.xsd.KeyStoreType.keyStoreTypeBuilder; +import static at.gv.zustellung.app2mzs.xsd.SSLType.SSLTypeBuilder; +import static java.util.stream.Collectors.toMap; @Component public class ConfigUtil { - private static final String TNVZ_REQUEST_KEY = "perform-query-person-request"; - private static final String MSG_URL_KEY = "msg.url"; + public static final String TNVZ_REQUEST_KEY = "perform-query-person-request"; + public static final String MSG_CLIENT_KEY = "msg-client"; + public static final String TNVZ_CLIENT_KEY = "tnvz-client"; + public static final String URL_KEY = "url"; + public static final String SSL_KEY = "ssl"; + public static final String TRUST_ALL_KEY = "trust-all"; + public static final String LAX_HOSTNAME_VERIFICATION_KEY = "lax-hostname-verification"; + public static final String KEYSTORE_KEY = "keystore"; + public static final String TRUSTSTORE_KEY = "truststore"; + public static final String FILENAME_KEY = "filename"; + public static final String FILETYPE_KEY = "filetype"; + public static final String PASSWORD_KEY = "password"; + /** * Convert a map into a Config object. @@ -23,16 +40,74 @@ public class ConfigUtil { * @return Config */ public ConfigType convert(Map values) { - var server = serverTypeBuilder() - .withZUSEUrlID(values.get(MSG_URL_KEY)) - .build(); - Boolean performQueryPersonRequest = values.get(TNVZ_REQUEST_KEY) == null ? null : Boolean.getBoolean(values.get(TNVZ_REQUEST_KEY)); + var msgClientParams = filterMapByPrefix(values, MSG_CLIENT_KEY); + ClientType msgClient = msgClientParams.isEmpty() + ? null : buildClient(msgClientParams); + + var tnvzClientParams = filterMapByPrefix(values, TNVZ_CLIENT_KEY); + ClientType tnvzClient = tnvzClientParams.isEmpty() + ? null : buildClient(tnvzClientParams); + return ConfigType.configTypeBuilder() .withPerformQueryPersonRequest(performQueryPersonRequest) - .withServer(server) + .withMSGClient(msgClient) + .withTNVZClient(tnvzClient) + .build(); + } + + private Map filterMapByPrefix(Map values, String prefix) { + return values.entrySet().stream() + .filter(entry -> entry.getKey().startsWith(prefix)) + .collect(toMap(e -> StringUtils.removePrefix(e.getKey()), Map.Entry::getValue)); + } + + + private ClientType buildClient(Map clientParams) { + + var url = clientParams.get(URL_KEY); + + var sslParams = filterMapByPrefix(clientParams, SSL_KEY); + SSLType ssl = sslParams.isEmpty() + ? null : buildSSL(sslParams); + + return clientTypeBuilder().withURL(url).withSSL(ssl).build(); + + } + + private SSLType buildSSL(Map sslParams) { + + var keyStoreParams = filterMapByPrefix(sslParams, KEYSTORE_KEY); + KeyStoreType keyStore = keyStoreParams.isEmpty() + ? null : buildKeyStore(keyStoreParams); + + var trustStoreParams = filterMapByPrefix(sslParams, TRUSTSTORE_KEY); + KeyStoreType trustStore = trustStoreParams.isEmpty() + ? null : buildKeyStore(trustStoreParams); + + var trustAll = sslParams.get(TRUST_ALL_KEY) == null + ? null : Boolean.getBoolean(sslParams.get(TRUST_ALL_KEY)); + + var laxHostNameVerification = sslParams.get(LAX_HOSTNAME_VERIFICATION_KEY) == null + ? null : Boolean.getBoolean(sslParams.get(LAX_HOSTNAME_VERIFICATION_KEY)); + + return SSLTypeBuilder() + .withKeyStore(keyStore) + .withTrustStore(trustStore) + .withTrustAll(trustAll) + .withLaxHostNameVerification(laxHostNameVerification) + .build(); + + } + + private KeyStoreType buildKeyStore(Map params) { + + return keyStoreTypeBuilder() + .withFileName(params.get(FILENAME_KEY)) + .withFileType(params.get(FILETYPE_KEY)) + .withPassword(params.get(PASSWORD_KEY)) .build(); } @@ -47,32 +122,65 @@ public class ConfigUtil { var builder = configTypeBuilder(fallback); - if(primary.getServer() != null) { - builder.withServer(merge(primary.getServer(), fallback.getServer())); + if (primary.isPerformQueryPersonRequest() != null) { + builder.withPerformQueryPersonRequest(primary.isPerformQueryPersonRequest()); } - if(primary.isPerformQueryPersonRequest() != null) { - builder.withPerformQueryPersonRequest(primary.isPerformQueryPersonRequest()); + if (primary.getMSGClient() != null) { + builder.withMSGClient(merge(primary.getMSGClient(), fallback.getMSGClient())); + } + + if (primary.getTNVZClient() != null) { + builder.withMSGClient(merge(primary.getTNVZClient(), fallback.getTNVZClient())); } return builder.build(); + } + + private ClientType merge(ClientType primary, ClientType fallback) { + var builder = clientTypeBuilder(fallback); + + if (primary.getURL() != null) { + builder.withURL(primary.getURL()); + } + + if (primary.getSSL() != null) { + builder.withSSL(merge(primary.getSSL(), fallback.getSSL())); + } + return builder.build(); } - private ServerType merge(ServerType primary, ServerType fallback) { + private SSLType merge(SSLType primary, SSLType fallback) { + var builder = SSLTypeBuilder(fallback); - if (fallback == null) { - return primary; + if (primary.getKeyStore() != null) { + builder.withKeyStore(merge(primary.getKeyStore(), fallback.getKeyStore())); } - var builder = serverTypeBuilder(fallback); + if (primary.getTrustStore() != null) { + builder.withKeyStore(merge(primary.getTrustStore(), fallback.getTrustStore())); + } - if (primary.getX509() != null) builder.withX509 (primary.getX509() ); - if (primary.getZUSEUrlID() != null) builder.withZUSEUrlID(primary.getZUSEUrlID()); + if (primary.isLaxHostNameVerification() != null) { + builder.withLaxHostNameVerification(primary.isLaxHostNameVerification()); + } + if (primary.isTrustAll() != null) { + builder.withLaxHostNameVerification(primary.isTrustAll()); + } return builder.build(); } + private KeyStoreType merge(KeyStoreType primary, KeyStoreType fallback) { + + if (primary.getFileName() != null && primary.getFileType() != null && primary.getPassword() != null) + return primary; + + return fallback; + + } + /** * Check if all mandatory fields are set. * @@ -80,11 +188,42 @@ public class ConfigUtil { * @return true if all mandatory fields are set */ public boolean isComplete(@Nullable ConfigType profile) { - //TODO: add check fo x509 certificate return profile != null && profile.isPerformQueryPersonRequest() != null - && profile.getServer() != null - && profile.getServer().getZUSEUrlID() != null; + && isTVNZClientConfigured(profile.getTNVZClient(), profile.isPerformQueryPersonRequest()) + && isMSGClientConfigured(profile.getMSGClient()); + } + + private boolean isTVNZClientConfigured(ClientType tnvzClient, Boolean isPerformQueryPersonRequest) { + return (tnvzClient != null + && tnvzClient.getURL() != null + && isSSLConfigured(tnvzClient)) + || isPerformQueryPersonRequest == false; } + private boolean isMSGClientConfigured(ClientType msgClient) { + return msgClient != null + && msgClient.getURL() != null + && isSSLConfigured(msgClient); + } + + private boolean isSSLConfigured(ClientType params) { + return (params.getURL().startsWith("https") + && params.getSSL() != null + && params.getSSL().isTrustAll() != null + && params.getSSL().isLaxHostNameVerification() != null + && isKeyStoreConfigured(params.getSSL().getKeyStore()) + && isKeyStoreConfigured(params.getSSL().getTrustStore())) + || !params.getURL().startsWith("https"); + } + + private boolean isKeyStoreConfigured(KeyStoreType keyStore) { + return (keyStore != null + && keyStore.getPassword() != null + && keyStore.getFileType() != null + && keyStore.getFileName() != null) + || keyStore == null; + } + + } diff --git a/src/main/java/at/gv/egiz/moazs/preprocess/DeliveryRequestAugmenter.java b/src/main/java/at/gv/egiz/moazs/preprocess/DeliveryRequestAugmenter.java index 057c3d4..d3891e4 100644 --- a/src/main/java/at/gv/egiz/moazs/preprocess/DeliveryRequestAugmenter.java +++ b/src/main/java/at/gv/egiz/moazs/preprocess/DeliveryRequestAugmenter.java @@ -37,8 +37,8 @@ public class DeliveryRequestAugmenter { public DeliveryRequestType augment(DeliveryRequestType request) { var requestConfig = request.getConfig(); - var profileId = determineProfileIdFrom(requestConfig); - var fallbackConfig = configs.get(profileId); + var fallbackProfileId = determineProfileIdFrom(requestConfig); + var fallbackConfig = configs.get(fallbackProfileId); if (fallbackConfig == null) { @@ -58,7 +58,7 @@ public class DeliveryRequestAugmenter { .withConfig(mergedConfig) .build(); } else { - throw moaZSException(INCOMPLETE_MERGED_CONFIG_ERROR_MESSAGE, profileId); + throw moaZSException(INCOMPLETE_MERGED_CONFIG_ERROR_MESSAGE, fallbackProfileId); } } } diff --git a/src/main/java/at/gv/egiz/moazs/util/FileUtils.java b/src/main/java/at/gv/egiz/moazs/util/FileUtils.java new file mode 100644 index 0000000..7e7723d --- /dev/null +++ b/src/main/java/at/gv/egiz/moazs/util/FileUtils.java @@ -0,0 +1,22 @@ +package at.gv.egiz.moazs.util; + +import org.springframework.stereotype.Component; + +import java.io.File; + +@Component +public class FileUtils { + + /** + * If path is relative, resolve path as classpath resource. If path is absolute, + * leave as-is. + */ + public String determinePath(String abstractPath) { + if (new File(abstractPath).isAbsolute()) { + return abstractPath; + } else { + //java.lang.Class needs relative resources to start with "/" + return this.getClass().getResource("/" + abstractPath).getFile(); + } + } +} diff --git a/src/main/java/at/gv/egiz/moazs/util/SSLContextCreator.java b/src/main/java/at/gv/egiz/moazs/util/SSLContextCreator.java new file mode 100644 index 0000000..b4d66d1 --- /dev/null +++ b/src/main/java/at/gv/egiz/moazs/util/SSLContextCreator.java @@ -0,0 +1,82 @@ +package at.gv.egiz.moazs.util; + +import at.gv.egiz.eaaf.core.impl.utils.KeyStoreUtils; +import at.gv.zustellung.app2mzs.xsd.KeyStoreType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Component; + +import javax.net.ssl.*; +import java.io.IOException; +import java.security.*; + +import static at.gv.egiz.moazs.MoaZSException.moaZSException; +import static java.lang.String.format; + +@Component +public class SSLContextCreator { + + private static final Logger log = LoggerFactory.getLogger(SSLContextCreator.class); + + /** + * Creates an SSL Context. + * Adapted from at.asitplus.eidas.specific.modules.authmodule_eIDASv2.szr.SZRClient + * + * @param keystore (if null, use no key store) + * @param truststore (if null, use default trust store) + * @throws at.gv.egiz.moazs.MoaZSException + */ + public SSLContext createSSLContext(@Nullable KeyStoreType keystore, @Nullable KeyStoreType truststore) { + try { + SSLContext context = SSLContext.getInstance("TLS"); + KeyManager[] keyManager = initKeyManager(keystore); + TrustManager[] trustManager = initTrustManager(truststore); + context.init(keyManager, trustManager, new SecureRandom()); + return context; + } catch (NoSuchAlgorithmException | KeyManagementException e) { + throw moaZSException("SSLContext initialization FAILED.", e); + } + } + + private KeyManager[] initKeyManager(KeyStoreType keystore) { + if (keystore == null) { + log.trace("No keystore path provided. NOT using SSL client authentication. "); + return null; + } else { + log.trace("Find keystore path: {}. Injecting SSL client certificate... ", keystore.getFileName()); + try { + KeyStore keyStore = KeyStoreUtils.loadKeyStore( + keystore.getFileType(), keystore.getFileName(), keystore.getPassword()); + KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); + kmf.init(keyStore, keystore.getPassword().toCharArray()); + log.trace("SSL client certificate injected."); + return kmf.getKeyManagers(); + } catch (IOException | GeneralSecurityException e) { + throw moaZSException(format("Can NOT load SSL client certificate from path: %s.", + keystore.getFileName()), e); + } + } + } + + private TrustManager[] initTrustManager(KeyStoreType truststore) { + if (truststore == null) { + log.trace("Using default truststore. "); + return null; + } else { + log.trace("Find truststore path: {}. Injecting SSL truststore... ", truststore.getFileName()); + try { + KeyStore trustStore = KeyStoreUtils.loadKeyStore( + truststore.getFileType(), truststore.getFileName(), truststore.getPassword()); + TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509"); + tmf.init(trustStore); + log.trace("SSL TrustStore injected to client. "); + return tmf.getTrustManagers(); + } catch (GeneralSecurityException | IOException e) { + throw moaZSException(format("Can NOT open SSL TrustStore from path: %s.", + truststore.getFileName()), e); + } + } + } + +} diff --git a/src/main/java/at/gv/egiz/moazs/util/StringUtils.java b/src/main/java/at/gv/egiz/moazs/util/StringUtils.java new file mode 100644 index 0000000..fd40f6e --- /dev/null +++ b/src/main/java/at/gv/egiz/moazs/util/StringUtils.java @@ -0,0 +1,17 @@ +package at.gv.egiz.moazs.util; + +public class StringUtils { + + public static boolean hasPrefix(String name) { + return name.indexOf('.') != -1; + } + + public static String keepPrefix(String name) { + return name.substring(0, name.indexOf('.')); + } + + public static String removePrefix(String name) { + return name.substring(name.indexOf('.') + 1); + } + +} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 9ce1158..a0040ca 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -13,32 +13,34 @@ spring: # Order: DeliveryRequest/Config > [chosen-profile] > default delivery-request-configuration-profiles: default: + + perform-query-person-request: false + ## All parameters for MSG client. - msg: + msg-client: - ## How to reach url: http://localhost:8081/services/DeliveryRequest + ssl: + ## Boolean; if true, app will trust all server certificates; + ## if false, server certificate needs to be in truststore. + trust-all: false + + ## Boolean; if true, app ignores mismatches between server's host name and + ## Certificate's common name / alternative subject name. + lax-hostname-verification: false + ## Parameters for ssl client auth keystore: ## Absolute path to file - filename: + filename: ssl/client.jks ## Password to unlock key store. password: 1233 ## JKS or PKCS12 type: JKS - ## Boolean; if true, app will trust all server certificates; - ## if false, server certificate needs to be in truststore. - trustall: false - ## Boolean; if true, app ignores mismatches between server's host name and - ## Certificate's common name / alternative subject name. - laxhostnameverification: false - - - perform-query-person-request: false app-profile-1: msg: @@ -49,9 +51,6 @@ delivery-request-configuration-profiles: msg: url: https://msg-url2.com -key-store-profiles: - msg-key-store: - ## If set to false, moa zs ignores an incomplete default DeliveryRequest-configuration ## profile and continues startup. See 'delivery-request-configuration-profiles'. ## Default value: true diff --git a/src/main/resources/mzs/app2mzs.xsd b/src/main/resources/mzs/app2mzs.xsd index 05a9ea4..956cd31 100644 --- a/src/main/resources/mzs/app2mzs.xsd +++ b/src/main/resources/mzs/app2mzs.xsd @@ -81,19 +81,39 @@ - + + - - + + + - - + + + + + + + + + + + + + + + + + + + + - + @@ -179,7 +199,7 @@ - + diff --git a/src/test/java/at/gv/egiz/moazs/DeliveryRequestAugmenterTest.java b/src/test/java/at/gv/egiz/moazs/DeliveryRequestAugmenterTest.java index c389b61..4bd64ef 100644 --- a/src/test/java/at/gv/egiz/moazs/DeliveryRequestAugmenterTest.java +++ b/src/test/java/at/gv/egiz/moazs/DeliveryRequestAugmenterTest.java @@ -4,19 +4,36 @@ import at.gv.egiz.moazs.preprocess.ConfigUtil; import at.gv.egiz.moazs.preprocess.DeliveryRequestAugmenter; import at.gv.zustellung.app2mzs.xsd.ConfigType; import at.gv.zustellung.app2mzs.xsd.DeliveryRequestType; +import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; import java.util.Map; +import static at.gv.zustellung.app2mzs.xsd.ClientType.clientTypeBuilder; import static at.gv.zustellung.app2mzs.xsd.ConfigType.configTypeBuilder; import static at.gv.zustellung.app2mzs.xsd.DeliveryRequestType.deliveryRequestTypeBuilder; -import static at.gv.zustellung.app2mzs.xsd.ServerType.serverTypeBuilder; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +@RunWith(MockitoJUnitRunner.class) public class DeliveryRequestAugmenterTest { private final String msgUrl = "http://mzs"; + @Mock + private ConfigUtil configUtil; + + @Before + public void setupMock() { + when(configUtil.isComplete(Mockito.any())).thenReturn(true); + when(configUtil.merge(any(), any())).thenCallRealMethod(); + } + @Test public void augmentPrimaryWithoutConfig() { var fallback = createConfig(msgUrl, false); @@ -88,11 +105,11 @@ public class DeliveryRequestAugmenterTest { } private DeliveryRequestAugmenter createAugmenter(ConfigType fallback) { - return new DeliveryRequestAugmenter(Map.of("default", fallback), new ConfigUtil()); + return new DeliveryRequestAugmenter(Map.of("default", fallback), configUtil); } private DeliveryRequestAugmenter createAugmenter(Map profiles) { - return new DeliveryRequestAugmenter(profiles, new ConfigUtil()); + return new DeliveryRequestAugmenter(profiles, configUtil); } private ConfigType createConfig(String url, Boolean performTnvz) { @@ -101,12 +118,12 @@ public class DeliveryRequestAugmenterTest { private ConfigType createConfig(String url, Boolean performTnvz, String profileId) { - var server = serverTypeBuilder() - .withZUSEUrlID(url) + var msgClient = clientTypeBuilder() + .withURL(url) .build(); return configTypeBuilder() - .withServer(server) + .withMSGClient(msgClient) .withPerformQueryPersonRequest(performTnvz) .withProfileID(profileId) .build(); diff --git a/src/test/java/at/gv/egiz/moazs/MsgClientTest.java b/src/test/java/at/gv/egiz/moazs/MsgClientTest.java index 8cebf06..62df52d 100644 --- a/src/test/java/at/gv/egiz/moazs/MsgClientTest.java +++ b/src/test/java/at/gv/egiz/moazs/MsgClientTest.java @@ -4,24 +4,18 @@ import at.gv.egiz.moazs.msg.MsgClient; import at.gv.egiz.moazs.msg.MsgClientFactory; import at.gv.egiz.moazs.msg.StoreSOAPBodyBinaryInRepositoryInterceptor; import at.gv.egiz.moazs.scheme.Marshaller; -import at.gv.zustellung.app2mzs.xsd.ConfigType; +import at.gv.zustellung.app2mzs.xsd.ClientType; import at.gv.zustellung.msg.xsd.DeliveryRequestType; import at.gv.zustellung.msg.xsd.ObjectFactory; -import org.junit.runner.RunWith; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.junit4.SpringRunner; import javax.xml.bind.JAXBElement; import java.io.BufferedInputStream; import java.io.FileInputStream; import java.io.IOException; -import static at.gv.zustellung.app2mzs.xsd.ConfigType.configTypeBuilder; -import static at.gv.zustellung.app2mzs.xsd.ServerType.serverTypeBuilder; - // @RunWith(SpringRunner.class) // @SpringBootTest @@ -52,11 +46,11 @@ public class MsgClientTest { public void sendValidMessage() throws IOException { var request = loadFromFile("validDeliveryRequest.xml"); - var config = generateConfig(httpServiceUri); - var client = factory.create(request, config, interceptor); + var clientParams = generateClientParams(httpServiceUri); + var client = factory.create(clientParams); try{ - var status = client.send(); + var status = client.send(request); logger.info("status: " + msgMarshaller.marshallXml(OF.createDeliveryResponse(status))); } catch (Exception ex) { System.out.println(ex.getMessage()); @@ -67,10 +61,10 @@ public class MsgClientTest { public void sendValidMessageToSSL() throws IOException { var request = loadFromFile("validDeliveryRequest.xml"); - var config = generateConfig(sslServiceUri); - var client = factory.create(request, config, interceptor); + var clientParams = generateClientParams(sslServiceUri); + var client = factory.create(clientParams); - var status = client.send(); + var status = client.send(request); logger.info("status: " + msgMarshaller.marshallXml(OF.createDeliveryRequestStatus(status))); } @@ -82,17 +76,8 @@ public class MsgClientTest { } } - private ConfigType generateConfig(String zuseUrl) { - - var server = serverTypeBuilder() - .withZUSEUrlID(zuseUrl) - .build(); - - return configTypeBuilder() - .withServer(server) - .withPerformQueryPersonRequest(false) - .build(); - + private ClientType generateClientParams(String url) { + return ClientType.clientTypeBuilder().withURL(url).build(); } } diff --git a/src/test/java/at/gv/egiz/moazs/SameThreadDeliveryPipelineTest.java b/src/test/java/at/gv/egiz/moazs/SameThreadDeliveryPipelineTest.java index cd454f2..df54ef7 100644 --- a/src/test/java/at/gv/egiz/moazs/SameThreadDeliveryPipelineTest.java +++ b/src/test/java/at/gv/egiz/moazs/SameThreadDeliveryPipelineTest.java @@ -2,7 +2,6 @@ package at.gv.egiz.moazs; import at.gv.egiz.moazs.msg.MsgClient; import at.gv.egiz.moazs.msg.MsgClientFactory; -import at.gv.egiz.moazs.verify.MoaSPSSSignatureVerifier; import at.gv.egiz.moazs.msg.StoreSOAPBodyBinaryInRepositoryInterceptor; import at.gv.egiz.moazs.pipeline.DeliveryPipeline; import at.gv.egiz.moazs.pipeline.SameThreadDeliveryPipeline; @@ -11,6 +10,7 @@ import at.gv.egiz.moazs.repository.InMemoryDeliveryRepository; import at.gv.egiz.moazs.scheme.Mzs2MsgConverter; import at.gv.egiz.moazs.tnvz.TnvzClient; import at.gv.egiz.moazs.tnvz.TnvzResultVerifier; +import at.gv.egiz.moazs.verify.MoaSPSSSignatureVerifier; import at.gv.zustellung.app2mzs.xsd.DeliveryRequestType; import at.gv.zustellung.msg.xsd.DeliveryRequestStatusType; import at.gv.zustellung.msg.xsd.MetaData; @@ -20,17 +20,16 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; import java.util.List; import static at.gv.egiz.moazs.MoaZSException.moaZSException; +import static at.gv.zustellung.app2mzs.xsd.ClientType.clientTypeBuilder; import static at.gv.zustellung.app2mzs.xsd.ConfigType.configTypeBuilder; import static at.gv.zustellung.app2mzs.xsd.DeliveryRequestType.Payload; import static at.gv.zustellung.app2mzs.xsd.DeliveryRequestType.Payload.payloadBuilder; import static at.gv.zustellung.app2mzs.xsd.DeliveryRequestType.deliveryRequestTypeBuilder; -import static at.gv.zustellung.app2mzs.xsd.ServerType.serverTypeBuilder; import static at.gv.zustellung.msg.xsd.DeliveryRequestStatusType.Success.successBuilder; import static at.gv.zustellung.msg.xsd.DeliveryRequestStatusType.deliveryRequestStatusTypeBuilder; import static at.gv.zustellung.msg.xsd.ErrorInfoType.errorInfoTypeBuilder; @@ -174,8 +173,8 @@ public class SameThreadDeliveryPipelineTest { when(tnvzClient.query(any(), any())).thenReturn(setupTnvzSuccess(acceptedTypes)); when(converter.convert(eq(mzsRequest) )).thenReturn(msgRequest); when(converter.convert(eq(mzsRequest), any())).thenReturn(msgRequest); - when(msgClientFactory.create(msgRequest, mzsRequest.getConfig(), interceptor)).thenReturn(msgClient); - when(msgClient.send()).thenReturn(status); + when(msgClientFactory.create(any())).thenReturn(msgClient); + when(msgClient.send(msgRequest)).thenReturn(status); return status; @@ -222,13 +221,13 @@ public class SameThreadDeliveryPipelineTest { private DeliveryRequestType setupMzsRequest(String appDeliveryId, boolean tnvzRequest, List mimeTypes) { - var server = serverTypeBuilder() - .withZUSEUrlID("http://zuse") + var msgClient = clientTypeBuilder() + .withURL("http://zuse") .build(); var config = configTypeBuilder() .withPerformQueryPersonRequest(tnvzRequest) - .withServer(server) + .withMSGClient(msgClient) .build(); return deliveryRequestTypeBuilder() -- cgit v1.2.3