/* * Copyright 2015 JBoss Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.apiman.gateway.platforms.servlet.auth.tls; import io.apiman.common.config.options.TLSOptions; import io.apiman.gateway.engine.IApiConnection; import io.apiman.gateway.engine.IApiConnectionResponse; import io.apiman.gateway.engine.IApiConnector; import io.apiman.gateway.engine.async.IAsyncResult; import io.apiman.gateway.engine.async.IAsyncResultHandler; import io.apiman.gateway.engine.auth.RequiredAuthType; import io.apiman.gateway.engine.beans.Api; import io.apiman.gateway.engine.beans.ApiRequest; import io.apiman.gateway.engine.beans.exceptions.ConnectorException; import io.apiman.gateway.platforms.servlet.connectors.HttpConnectorFactory; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.math.BigInteger; import java.net.URISyntaxException; import java.net.URL; import java.nio.file.Paths; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; import javax.net.ssl.SSLSocket; import javax.security.cert.CertificateException; import javax.security.cert.X509Certificate; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.SecureRequestCustomizer; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.SslConnectionFactory; import org.eclipse.jetty.server.handler.AbstractHandler; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; /** * Important note from {@link SSLSocket#getNeedClientAuth()} about requiring client auth: * <p> * <q>... if this option is set and the client chooses not to provide authentication information about itself, * the negotiations will stop and the engine will begin its closure procedure.</q> * <p> * Hence we often capture an {@link ConnectorException} in tests when 2-way auth is failed. * * @author Marc Savy <msavy@redhat.com> */ @SuppressWarnings("nls") public class BasicMutualAuthTest { private Server server; private HttpConfiguration http_config; private Map<String, String> config = new HashMap<>(); //private java.security.cert.X509Certificate clientCertUsed; @Rule public ExpectedException exception = ExpectedException.none(); protected BigInteger clientSerial; /** * With thanks to assistance of http://stackoverflow.com/b/20056601/2766538 * @throws Exception any exception */ @Before public void setupJetty() throws Exception { server = new Server(); server.setStopAtShutdown(true); http_config = new HttpConfiguration(); http_config.setSecureScheme("https"); SslContextFactory sslContextFactory = new SslContextFactory(); sslContextFactory.setKeyStorePath(getResourcePath("2waytest/basic_mutual_auth/service_ks.jks")); sslContextFactory.setKeyStorePassword("password"); sslContextFactory.setKeyManagerPassword("password"); sslContextFactory.setTrustStorePath(getResourcePath("2waytest/basic_mutual_auth/service_ts.jks")); sslContextFactory.setTrustStorePassword("password"); sslContextFactory.setNeedClientAuth(true); HttpConfiguration https_config = new HttpConfiguration(http_config); https_config.addCustomizer(new SecureRequestCustomizer()); ServerConnector sslConnector = new ServerConnector(server, new SslConnectionFactory(sslContextFactory,"http/1.1"), new HttpConnectionFactory(https_config)); sslConnector.setPort(8008); server.addConnector(sslConnector); // Thanks to Jetty getting started guide. server.setHandler(new AbstractHandler() { @Override public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { Enumeration<String> z = request.getAttributeNames(); while (z.hasMoreElements()) { String elem = z.nextElement(); System.out.println(elem + " - " + request.getAttribute(elem)); } if (request.getAttribute("javax.servlet.request.X509Certificate") != null) { clientSerial = ((java.security.cert.X509Certificate[]) request .getAttribute("javax.servlet.request.X509Certificate"))[0].getSerialNumber(); } response.setStatus(HttpServletResponse.SC_OK); baseRequest.setHandled(true); response.getWriter().println("apiman"); } }); server.start(); } @After public void destroyJetty() throws Exception { server.stop(); server.destroy(); config.clear(); } ApiRequest request = new ApiRequest(); Api api = new Api(); { request.setApiKey("12345"); request.setDestination("/"); request.getHeaders().put("test", "it-worked"); request.setTransportSecure(true); request.setRemoteAddr("https://localhost:8008/"); request.setType("GET"); api.setEndpoint("https://localhost:8008/"); api.getEndpointProperties().put(RequiredAuthType.ENDPOINT_AUTHORIZATION_TYPE, "mtls"); } /** * Scenario: * - no CA inherited trust * - gateway trusts API certificate directly * - API trusts gateway certificate directly */ @Test public void shouldSucceedWithValidMTLS() { config.put(TLSOptions.TLS_TRUSTSTORE, getResourcePath("2waytest/basic_mutual_auth_2/gateway_ts.jks")); config.put(TLSOptions.TLS_TRUSTSTOREPASSWORD, "password"); config.put(TLSOptions.TLS_KEYSTORE, getResourcePath("2waytest/basic_mutual_auth_2/gateway_ks.jks")); config.put(TLSOptions.TLS_KEYSTOREPASSWORD, "password"); config.put(TLSOptions.TLS_KEYPASSWORD, "password"); config.put(TLSOptions.TLS_ALLOWANYHOST, "true"); config.put(TLSOptions.TLS_ALLOWSELFSIGNED, "false"); HttpConnectorFactory factory = new HttpConnectorFactory(config); IApiConnector connector = factory.createConnector(request, api, RequiredAuthType.MTLS, false); IApiConnection connection = connector.connect(request, new IAsyncResultHandler<IApiConnectionResponse>() { @Override public void handle(IAsyncResult<IApiConnectionResponse> result) { if (result.isError()) throw new RuntimeException(result.getError()); Assert.assertTrue(result.isSuccess()); } }); connection.end(); } /** * Scenario: * - no CA inherited trust * - gateway does <em>not</em> trust the API * - API trusts gateway certificate */ @Test public void shouldFailWhenGatewayDoesNotTrustApi() { config.put(TLSOptions.TLS_TRUSTSTORE, getResourcePath("2waytest/basic_mutual_auth/gateway_ts.jks")); config.put(TLSOptions.TLS_TRUSTSTOREPASSWORD, "password"); config.put(TLSOptions.TLS_KEYSTORE, getResourcePath("2waytest/basic_mutual_auth/gateway_ks.jks")); config.put(TLSOptions.TLS_KEYSTOREPASSWORD, "password"); config.put(TLSOptions.TLS_KEYPASSWORD, "password"); config.put(TLSOptions.TLS_ALLOWANYHOST, "true"); config.put(TLSOptions.TLS_ALLOWSELFSIGNED, "false"); HttpConnectorFactory factory = new HttpConnectorFactory(config); IApiConnector connector = factory.createConnector(request, api, RequiredAuthType.MTLS, false); IApiConnection connection = connector.connect(request, new IAsyncResultHandler<IApiConnectionResponse>() { @Override public void handle(IAsyncResult<IApiConnectionResponse> result) { Assert.assertTrue(result.isError()); System.out.println(result.getError()); Assert.assertTrue(result.getError() instanceof ConnectorException); // Would like to assert on SSL error, but is sun specific info // TODO improve connector to handle this situation better } }); exception.expect(RuntimeException.class); connection.end(); } /** * Scenario: * - no CA inherited trust * - gateway does trust the API * - API does <em>not</em> trust gateway */ @Test public void shouldFailWhenApiDoesNotTrustGateway() { config.put(TLSOptions.TLS_TRUSTSTORE, getResourcePath("2waytest/service_not_trust_gw/gateway_ts.jks")); config.put(TLSOptions.TLS_TRUSTSTOREPASSWORD, "password"); config.put(TLSOptions.TLS_KEYSTORE, getResourcePath("2waytest/service_not_trust_gw/gateway_ks.jks")); config.put(TLSOptions.TLS_KEYSTOREPASSWORD, "password"); config.put(TLSOptions.TLS_KEYPASSWORD, "password"); config.put(TLSOptions.TLS_ALLOWANYHOST, "true"); config.put(TLSOptions.TLS_ALLOWSELFSIGNED, "false"); HttpConnectorFactory factory = new HttpConnectorFactory(config); IApiConnector connector = factory.createConnector(request, api, RequiredAuthType.MTLS, false); IApiConnection connection = connector.connect(request, new IAsyncResultHandler<IApiConnectionResponse>() { @Override public void handle(IAsyncResult<IApiConnectionResponse> result) { Assert.assertTrue(result.isError()); System.out.println(result.getError()); Assert.assertTrue(result.getError() instanceof ConnectorException); // Would like to assert on SSL error, but is sun specific info // TODO improve connector to handle this situation better } }); exception.expect(RuntimeException.class); connection.end(); } /** * Scenario: * - no CA inherited trust * - gateway does not explicitly trust the API, but automatically validates against self-signed * - API trusts gateway certificate */ @Test public void shouldSucceedWhenAllowedSelfSigned() { config.put(TLSOptions.TLS_TRUSTSTORE, getResourcePath("2waytest/basic_mutual_auth/gateway_ts.jks")); config.put(TLSOptions.TLS_TRUSTSTOREPASSWORD, "password"); config.put(TLSOptions.TLS_KEYSTORE, getResourcePath("2waytest/basic_mutual_auth/gateway_ks.jks")); config.put(TLSOptions.TLS_KEYSTOREPASSWORD, "password"); config.put(TLSOptions.TLS_KEYPASSWORD, "password"); config.put(TLSOptions.TLS_ALLOWANYHOST, "true"); config.put(TLSOptions.TLS_ALLOWSELFSIGNED, "true"); HttpConnectorFactory factory = new HttpConnectorFactory(config); IApiConnector connector = factory.createConnector(request, api, RequiredAuthType.MTLS, false); IApiConnection connection = connector.connect(request, new IAsyncResultHandler<IApiConnectionResponse>() { @Override public void handle(IAsyncResult<IApiConnectionResponse> result) { Assert.assertTrue(result.isSuccess()); } }); connection.end(); } /** * Scenario: * - Select client key alias `gateway2`. * - Mutual trust exists between gateway and API * - We must use the `gateway2` cert NOT `gateway`. * @throws CertificateException the certificate exception * @throws IOException the IO exception */ @Test public void shouldSucceedWhenValidKeyAlias() throws CertificateException, IOException { config.put(TLSOptions.TLS_TRUSTSTORE, getResourcePath("2waytest/basic_mutual_auth_2/gateway_ts.jks")); config.put(TLSOptions.TLS_TRUSTSTOREPASSWORD, "password"); config.put(TLSOptions.TLS_KEYSTORE, getResourcePath("2waytest/basic_mutual_auth_2/gateway_ks.jks")); config.put(TLSOptions.TLS_KEYSTOREPASSWORD, "password"); config.put(TLSOptions.TLS_KEYPASSWORD, "password"); config.put(TLSOptions.TLS_ALLOWANYHOST, "true"); config.put(TLSOptions.TLS_ALLOWSELFSIGNED, "false"); config.put(TLSOptions.TLS_KEYALIASES, "gateway2"); InputStream inStream = new FileInputStream(getResourcePath("2waytest/basic_mutual_auth_2/gateway2.cer")); final X509Certificate expectedCert = X509Certificate.getInstance(inStream); inStream.close(); HttpConnectorFactory factory = new HttpConnectorFactory(config); IApiConnector connector = factory.createConnector(request, api, RequiredAuthType.MTLS, false); IApiConnection connection = connector.connect(request, new IAsyncResultHandler<IApiConnectionResponse>() { @Override public void handle(IAsyncResult<IApiConnectionResponse> result) { if (result.isError()) throw new RuntimeException(result.getError()); Assert.assertTrue(result.isSuccess()); // Assert that the expected certificate (associated with the private key by virtue) // was the one used. Assert.assertEquals(expectedCert.getSerialNumber(), clientSerial); } }); connection.end(); } /** * Scenario: * - First alias invalid, second valid. * - Mutual trust exists between gateway and API. * - We must fall back to the valid alias. * @throws CertificateException the certificate exception * @throws IOException the IO exception */ @Test public void shouldFallbackWhenMultipleAliasesAvailable() throws CertificateException, IOException { config.put(TLSOptions.TLS_TRUSTSTORE, getResourcePath("2waytest/basic_mutual_auth_2/gateway_ts.jks")); config.put(TLSOptions.TLS_TRUSTSTOREPASSWORD, "password"); config.put(TLSOptions.TLS_KEYSTORE, getResourcePath("2waytest/basic_mutual_auth_2/gateway_ks.jks")); config.put(TLSOptions.TLS_KEYSTOREPASSWORD, "password"); config.put(TLSOptions.TLS_KEYPASSWORD, "password"); config.put(TLSOptions.TLS_ALLOWANYHOST, "true"); config.put(TLSOptions.TLS_ALLOWSELFSIGNED, "false"); // Only gateway2 is valid. `unrelated` is real but not trusted by API. others don't exist. config.put(TLSOptions.TLS_KEYALIASES, "unrelated, owt, or, nowt, gateway2, sonorous, unrelated"); InputStream inStream = new FileInputStream(getResourcePath("2waytest/basic_mutual_auth_2/gateway2.cer")); final X509Certificate expectedCert = X509Certificate.getInstance(inStream); inStream.close(); HttpConnectorFactory factory = new HttpConnectorFactory(config); IApiConnector connector = factory.createConnector(request, api, RequiredAuthType.MTLS, false); IApiConnection connection = connector.connect(request, new IAsyncResultHandler<IApiConnectionResponse>() { @Override public void handle(IAsyncResult<IApiConnectionResponse> result) { if (result.isError()) throw new RuntimeException(result.getError()); Assert.assertTrue(result.isSuccess()); // Assert that the expected certificate (associated with the private key by virtue) // was the one used. Assert.assertEquals(expectedCert.getSerialNumber(), clientSerial); } }); connection.end(); } /** * Scenario: * - Select invalid key alias (no such key). * - Negotiation will fail * @throws CertificateException the certificate exception * @throws IOException the IO exception */ @Test public void shouldFailWithInValidKeyAlias() throws CertificateException, IOException { config.put(TLSOptions.TLS_TRUSTSTORE, getResourcePath("2waytest/basic_mutual_auth_2/gateway_ts.jks")); config.put(TLSOptions.TLS_TRUSTSTOREPASSWORD, "password"); config.put(TLSOptions.TLS_KEYSTORE, getResourcePath("2waytest/basic_mutual_auth_2/gateway_ks.jks")); config.put(TLSOptions.TLS_KEYSTOREPASSWORD, "password"); config.put(TLSOptions.TLS_KEYPASSWORD, "password"); config.put(TLSOptions.TLS_ALLOWANYHOST, "true"); config.put(TLSOptions.TLS_ALLOWSELFSIGNED, "false"); // No such key exists in the keystore config.put(TLSOptions.TLS_KEYALIASES, "xxx"); HttpConnectorFactory factory = new HttpConnectorFactory(config); IApiConnector connector = factory.createConnector(request, api, RequiredAuthType.MTLS, false); IApiConnection connection = connector.connect(request, new IAsyncResultHandler<IApiConnectionResponse>() { @Override public void handle(IAsyncResult<IApiConnectionResponse> result) { Assert.assertTrue(result.isError()); } }); exception.expect(RuntimeException.class); connection.end(); } /** * Scenario: * - Development mode TLS pass-through. Gateway accepts anything. * - Server should still refuse on basis of requiring client auth. */ @Test public void shouldFailWithDevModeAndNoClientKeys() { config.put(TLSOptions.TLS_DEVMODE, "true"); HttpConnectorFactory factory = new HttpConnectorFactory(config); IApiConnector connector = factory.createConnector(request, api, RequiredAuthType.DEFAULT, false); IApiConnection connection = connector.connect(request, new IAsyncResultHandler<IApiConnectionResponse>() { @Override public void handle(IAsyncResult<IApiConnectionResponse> result) { Assert.assertTrue(result.isError()); System.out.println(result.getError()); } }); exception.expect(RuntimeException.class); connection.end(); } private String getResourcePath(String res) { URL resource = CAMutualAuthTest.class.getResource(res); try { return Paths.get(resource.toURI()).toFile().getAbsolutePath(); } catch (URISyntaxException e) { throw new RuntimeException(e); } } }