package cz.tomasdvorak.eet.client.security; import java.util.HashMap; import java.util.Map; import javax.xml.ws.BindingProvider; import cz.tomasdvorak.eet.client.exceptions.DnsLookupFailedException; import cz.tomasdvorak.eet.client.exceptions.DnsTimeoutException; import cz.tomasdvorak.eet.client.networking.DnsLookup; import cz.tomasdvorak.eet.client.networking.DnsLookupWithTimeout; import org.apache.cxf.endpoint.Client; import org.apache.cxf.frontend.ClientProxy; import org.apache.cxf.jaxws.JaxWsProxyFactoryBean; import org.apache.cxf.transport.http.HTTPConduit; import org.apache.cxf.transports.http.configuration.HTTPClientPolicy; import org.apache.cxf.ws.security.wss4j.WSS4JInInterceptor; import org.apache.cxf.ws.security.wss4j.WSS4JOutInterceptor; import org.apache.wss4j.dom.handler.WSHandlerConstants; import cz.etrzby.xml.EET; import cz.etrzby.xml.EETService; import cz.tomasdvorak.eet.client.config.EndpointType; import cz.tomasdvorak.eet.client.dto.WebserviceConfiguration; import cz.tomasdvorak.eet.client.logging.WebserviceLogging; import cz.tomasdvorak.eet.client.timing.TimingReceiveInterceptor; import cz.tomasdvorak.eet.client.timing.TimingSendInterceptor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class SecureEETCommunication { private static final Logger logger = LoggerFactory.getLogger(SecureEETCommunication.class); /** * Key used to store crypto instance in the configuration params of Merlin crypto instance. */ private static final String CRYPTO_INSTANCE_KEY = "eetCryptoInstance"; /** * System property holding keystore password. Either provided already, or set to "changeit" - the default password. */ private static final String JAVAX_NET_SSL_KEY_STORE_PASSWORD = "javax.net.ssl.keyStorePassword"; /** * Check EET's certificate for the following regex */ public static final String SUBJECT_CERT_CONSTRAINTS = ".*O=Česká republika - Generální finanční ředitelství.*"; /** * Service instance is thread safe and cachable, so create just one instance during initialization of the class */ private static final EETService WEBSERVICE = new EETService(); /** * Signing of data and requests */ private final ClientKey clientKey; /** * Validation of response signature */ private final ServerKey serverRootCa; /** * Webservice technical configuration - timeouts etc. */ private final WebserviceConfiguration wsConfiguration; protected SecureEETCommunication(final ClientKey clientKey, final ServerKey serverKey, final WebserviceConfiguration wsConfiguration) { this.clientKey = clientKey; this.serverRootCa = serverKey; this.wsConfiguration = wsConfiguration; } protected EET getPort(final EndpointType endpointType) throws DnsTimeoutException, DnsLookupFailedException { if (wsConfiguration.getDnsLookupTimeout() > 0) { final DnsLookup resolver = new DnsLookupWithTimeout(wsConfiguration.getDnsResolver(), wsConfiguration.getDnsLookupTimeout()); final String ip = resolver.resolveAddress(endpointType.getWebserviceUrl()); logger.info(String.format("DNS lookup resolved %s to %s", endpointType, ip)); } final JaxWsProxyFactoryBean factory = new JaxWsProxyFactoryBean(); factory.setServiceClass(EET.class); factory.getClientFactoryBean().getServiceFactory().setWsdlURL(WEBSERVICE.getWSDLDocumentLocation()); factory.setServiceName(WEBSERVICE.getServiceName()); final EET port = (EET) factory.create(); final Client clientProxy = ClientProxy.getClient(port); ensureHTTPSKeystorePassword(); configureEndpointUrl(port, endpointType.getWebserviceUrl()); configureSchemaValidation(port); configureTimeout(clientProxy); configureLogging(clientProxy); configureSigning(clientProxy); return port; } protected ClientKey getClientKey() { return clientKey; } private void ensureHTTPSKeystorePassword() { if (System.getProperty(JAVAX_NET_SSL_KEY_STORE_PASSWORD) == null) { // there is not set keystore password (needed for HTTPS communication handshake), set the usual default one // TODO: is this assumption ok? System.setProperty(JAVAX_NET_SSL_KEY_STORE_PASSWORD, "changeit"); } } /** * Sign our request with the client key par. */ private void configureSigning(final Client clientProxy) { final WSS4JOutInterceptor wssOut = createSigningInterceptor(); clientProxy.getOutInterceptors().add(wssOut); final WSS4JInInterceptor wssIn = createValidatingInterceptor(); clientProxy.getInInterceptors().add(wssIn); clientProxy.getInInterceptors().add(new SignatureFaultInterceptor()); } /** * Checks, if the response is signed by a key produced by CA, which do we accept (provided to this client) */ private WSS4JInInterceptor createValidatingInterceptor() { final Map<String, Object> inProps = new HashMap<String, Object>(); inProps.put(WSHandlerConstants.ACTION, WSHandlerConstants.SIGNATURE); // only sign, do not encrypt inProps.put(CRYPTO_INSTANCE_KEY, serverRootCa.getCrypto()); // provides I.CA root CA certificate inProps.put(WSHandlerConstants.SIG_PROP_REF_ID, CRYPTO_INSTANCE_KEY); inProps.put(WSHandlerConstants.SIG_SUBJECT_CERT_CONSTRAINTS, SUBJECT_CERT_CONSTRAINTS); // regex validation of the cert. inProps.put(WSHandlerConstants.ENABLE_REVOCATION, "true"); // activate CRL checks return new WSS4JEetInInterceptor(inProps); } private WSS4JOutInterceptor createSigningInterceptor() { final Map<String, Object> signingProperties = new HashMap<String, Object>(); signingProperties.put(WSHandlerConstants.ACTION, WSHandlerConstants.SIGNATURE); // only sign, do not encrypt signingProperties.put(WSHandlerConstants.PW_CALLBACK_REF, this.clientKey.getClientPasswordCallback()); signingProperties.put(WSHandlerConstants.SIGNATURE_USER, this.clientKey.getAlias()); // provides client keys to signing signingProperties.put(CRYPTO_INSTANCE_KEY, clientKey.getCrypto()); signingProperties.put(WSHandlerConstants.SIG_PROP_REF_ID, CRYPTO_INSTANCE_KEY); signingProperties.put(WSHandlerConstants.SIG_KEY_ID, "DirectReference"); // embed the public cert into requests signingProperties.put(WSHandlerConstants.SIG_ALGO, "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"); signingProperties.put(WSHandlerConstants.SIG_DIGEST_ALGO, "http://www.w3.org/2001/04/xmlenc#sha256"); return new WSS4JEetOutInterceptor(signingProperties); } private void configureTimeout(final Client clientProxy) { final HTTPConduit conduit = (HTTPConduit) clientProxy.getConduit(); final HTTPClientPolicy policy = new HTTPClientPolicy(); policy.setReceiveTimeout(this.wsConfiguration.getReceiveTimeout()); policy.setConnectionTimeout(this.wsConfiguration.getReceiveTimeout()); policy.setAsyncExecuteTimeout(this.wsConfiguration.getReceiveTimeout()); conduit.setClient(policy); } private void configureEndpointUrl(final EET remote, final String webserviceUrl) { final Map<String, Object> requestContext = ((BindingProvider) remote).getRequestContext(); requestContext.put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, webserviceUrl); } private void configureSchemaValidation(final EET remote) { final Map<String, Object> requestContext = ((BindingProvider) remote).getRequestContext(); requestContext.put("schema-validation-enabled", "true"); } /** * Logs all requests and responses of the WS communication (see log4j2.xml file for exact logging settings) */ private void configureLogging(final Client clientProxy) { clientProxy.getInInterceptors().add(WebserviceLogging.LOGGING_IN_INTERCEPTOR); clientProxy.getOutInterceptors().add(WebserviceLogging.LOGGING_OUT_INTERCEPTOR); clientProxy.getOutInterceptors().add(TimingSendInterceptor.INSTANCE); clientProxy.getInInterceptors().add(TimingReceiveInterceptor.INSTANCE); } }