Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
import org.apache.knox.gateway.services.registry.ServiceDefEntry;
import org.apache.knox.gateway.services.registry.ServiceDefinitionRegistry;
import org.apache.knox.gateway.services.registry.ServiceRegistry;
import org.apache.knox.gateway.services.security.AliasService;
import org.apache.knox.gateway.services.security.AliasServiceException;
import org.apache.knox.gateway.services.security.KeystoreService;
import org.apache.knox.gateway.services.security.KeystoreServiceException;
import org.apache.knox.gateway.webshell.WebshellWebSocketAdapter;
Expand Down Expand Up @@ -68,6 +70,12 @@ public class GatewayWebsocketHandler extends WebSocketHandler

static final String REGEX_SPLIT_SERVICE_PATH = "^((?:[^/]*/){3}[^/]*)";

static final String TRUSTSTORE_USER_PROPERTY = "org.apache.knox.gateway.websockets.truststore";

static final String KEYSTORE_USER_PROPERTY = "org.apache.knox.gateway.websockets.keystore";

static final String KEYSTORE_KEY_PASSPHRASE_USER_PROPERTY = "org.apache.knox.gateway.websockets.keystore.key.passphrase";

static final String REGEX_WEBSHELL_REQUEST_PATH =
"^(" + SECURE_WEBSOCKET_PROTOCOL_STRING+"|"+WEBSOCKET_PROTOCOL_STRING + ")[^/]+/[^/]+/webshell$";

Expand Down Expand Up @@ -150,7 +158,8 @@ public Object createWebSocket(ServletUpgradeRequest req,

// Upgrade happens here
final ClientEndpointConfig clientConfig = getClientEndpointConfig(req);
clientConfig.getUserProperties().put("org.apache.knox.gateway.websockets.truststore", getTruststore());
clientConfig.getUserProperties().put(TRUSTSTORE_USER_PROPERTY, getTruststore());
configureClientIdentity(clientConfig.getUserProperties());
return new ProxyWebSocketAdapter(URI.create(backendURL), pool, clientConfig, config);
} catch (final Exception e) {
LOG.failedCreatingWebSocket(e);
Expand All @@ -169,6 +178,38 @@ private KeyStore getTruststore() throws KeystoreServiceException {
return trustKeystore;
}

/**
* Mirrors DefaultHttpClientFactory#createSSLContext: when two-way SSL is
* enabled, select the client identity keystore (single-EKU aware) and add it,
* with its key passphrase, to the WebSocket client's user properties so the
* outbound TLS handshake can present a client certificate.
*/
void configureClientIdentity(final Map<String, Object> userProperties)
throws KeystoreServiceException, AliasServiceException {
if (!config.isHttpClientTwoWaySslEnabled()) {
return;
}

final KeystoreService ks = this.services.getService(ServiceType.KEYSTORE_SERVICE);
final AliasService as = this.services.getService(ServiceType.ALIAS_SERVICE);

final KeyStore identityKeystore;
final char[] identityKeyPassphrase;
if (config.isSingleEkuEnabled()) {
identityKeystore = ks.getKeystoreForHttpClient();
identityKeyPassphrase = as.getHttpClientKeyPassphrase();
} else {
identityKeystore = ks.getKeystoreForGateway();
identityKeyPassphrase = as.getGatewayIdentityPassphrase();
}

if (identityKeystore != null) {
userProperties.put(KEYSTORE_USER_PROPERTY, identityKeystore);
userProperties.put(KEYSTORE_KEY_PASSPHRASE_USER_PROPERTY, identityKeyPassphrase);
} else {
LOG.noClientIdentityForTwoWaySsl();
}
}

/**
* Returns a {@link ClientEndpointConfig} config that contains the headers
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import java.net.URI;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
Expand All @@ -35,6 +36,7 @@
import org.apache.knox.gateway.config.GatewayConfig;
import org.eclipse.jetty.io.RuntimeIOException;
import org.eclipse.jetty.util.component.LifeCycle;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.websocket.api.BatchMode;
import org.eclipse.jetty.websocket.api.RemoteEndpoint;
import org.eclipse.jetty.websocket.api.Session;
Expand Down Expand Up @@ -100,21 +102,17 @@ public void onWebSocketConnect(final Session frontEndSession) {
container.setAsyncSendTimeout(frontEndSession.getPolicy().getAsyncWriteTimeout());
container.setDefaultMaxSessionIdleTimeout(frontEndSession.getPolicy().getIdleTimeout());

KeyStore ks = null;
if(clientConfig != null) {
ks = (KeyStore) clientConfig.getUserProperties().get("org.apache.knox.gateway.websockets.truststore");
}

/*
Currently javax.websocket API has no provisions to configure SSL
https://github.com/eclipse-ee4j/websocket-api/issues/210
Until that gets fixed we'll have to resort to this.
*/
if(container instanceof org.eclipse.jetty.websocket.jsr356.ClientContainer &&
if(clientConfig != null &&
container instanceof org.eclipse.jetty.websocket.jsr356.ClientContainer &&
((org.eclipse.jetty.websocket.jsr356.ClientContainer)container).getClient() != null &&
((org.eclipse.jetty.websocket.jsr356.ClientContainer)container).getClient().getSslContextFactory() != null ) {
((org.eclipse.jetty.websocket.jsr356.ClientContainer)container).getClient().getHttpClient().getSslContextFactory().setTrustStore(ks);
LOG.logMessage("Truststore for websocket setup");
configureSsl(((org.eclipse.jetty.websocket.jsr356.ClientContainer)container).getClient().getHttpClient().getSslContextFactory(), clientConfig);
LOG.logMessage("SSL for websocket setup");
}

final ProxyInboundClient backendSocket = new ProxyInboundClient(getMessageCallback());
Expand Down Expand Up @@ -160,6 +158,31 @@ public void onWebSocketConnect(final Session frontEndSession) {
}
}

/**
* Configures the WebSocket client's SslContextFactory from the values the
* handler placed in the ClientEndpointConfig user properties: the truststore
* (unchanged behavior, may be null) and, when two-way SSL supplied one, the
* client identity keystore plus its key-manager password.
*/
static void configureSsl(final SslContextFactory sslContextFactory,
final ClientEndpointConfig clientConfig) {
final Map<String, Object> userProperties = clientConfig.getUserProperties();

sslContextFactory.setTrustStore(
(KeyStore) userProperties.get(GatewayWebsocketHandler.TRUSTSTORE_USER_PROPERTY));

final KeyStore identityKeystore =
(KeyStore) userProperties.get(GatewayWebsocketHandler.KEYSTORE_USER_PROPERTY);
if (identityKeystore != null) {
sslContextFactory.setKeyStore(identityKeystore);
final char[] passphrase =
(char[]) userProperties.get(GatewayWebsocketHandler.KEYSTORE_KEY_PASSPHRASE_USER_PROPERTY);
if (passphrase != null) {
sslContextFactory.setKeyManagerPassword(new String(passphrase));
}
}
}

@Override
public void onWebSocketBinary(final byte[] payload, final int offset, final int length) {
if (isNotConnected()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,9 @@ void failedCreatingWebSocket(
text = "{0}")
void debugLog(String message);

@Message(level = MessageLevel.WARN,
text = "Two-way SSL is enabled but no client identity keystore was found; "
+ "the outbound WebSocket connection will not present a client certificate")
void noClientIdentityForTwoWaySsl();

}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
import org.apache.knox.gateway.i18n.messages.MessagesFactory;
import org.apache.knox.gateway.provider.federation.jwt.JWTMessages;
import org.apache.knox.gateway.services.GatewayServices;
import org.apache.knox.gateway.services.ServiceType;
import org.apache.knox.gateway.services.security.AliasService;
import org.apache.knox.gateway.services.security.KeystoreService;
import org.apache.knox.gateway.webshell.WebshellWebSocketAdapter;
import org.easymock.EasyMock;
import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
Expand All @@ -44,8 +47,10 @@
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

import java.security.KeyStore;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ExecutorService;
Expand Down Expand Up @@ -168,6 +173,100 @@ public void testDisabledWebShell() throws Exception{
gatewayWebsocketHandler.createWebSocket(req,resp);
}

@Test
public void testConfigureClientIdentityTwoWaySslGatewayIdentity() throws Exception {
GatewayConfig gatewayConfig = EasyMock.createNiceMock(GatewayConfig.class);
EasyMock.expect(gatewayConfig.isHttpClientTwoWaySslEnabled()).andReturn(true).anyTimes();
EasyMock.expect(gatewayConfig.isSingleEkuEnabled()).andReturn(false).anyTimes();

KeyStore identity = KeyStore.getInstance(KeyStore.getDefaultType());
identity.load(null, null);
char[] passphrase = "gateway-secret".toCharArray();

KeystoreService keystoreService = EasyMock.createNiceMock(KeystoreService.class);
EasyMock.expect(keystoreService.getKeystoreForGateway()).andReturn(identity).anyTimes();
AliasService aliasService = EasyMock.createNiceMock(AliasService.class);
EasyMock.expect(aliasService.getGatewayIdentityPassphrase()).andReturn(passphrase).anyTimes();

GatewayServices services = EasyMock.createNiceMock(GatewayServices.class);
EasyMock.expect(services.getService(ServiceType.KEYSTORE_SERVICE)).andReturn(keystoreService).anyTimes();
EasyMock.expect(services.getService(ServiceType.ALIAS_SERVICE)).andReturn(aliasService).anyTimes();
EasyMock.replay(gatewayConfig, keystoreService, aliasService, services);

GatewayWebsocketHandler handler = new GatewayWebsocketHandler(gatewayConfig, services);
Map<String, Object> props = new HashMap<>();
handler.configureClientIdentity(props);

Assert.assertSame(identity, props.get(GatewayWebsocketHandler.KEYSTORE_USER_PROPERTY));
Assert.assertSame(passphrase, props.get(GatewayWebsocketHandler.KEYSTORE_KEY_PASSPHRASE_USER_PROPERTY));
}

@Test
public void testConfigureClientIdentityTwoWaySslSingleEku() throws Exception {
GatewayConfig gatewayConfig = EasyMock.createNiceMock(GatewayConfig.class);
EasyMock.expect(gatewayConfig.isHttpClientTwoWaySslEnabled()).andReturn(true).anyTimes();
EasyMock.expect(gatewayConfig.isSingleEkuEnabled()).andReturn(true).anyTimes();

KeyStore clientIdentity = KeyStore.getInstance(KeyStore.getDefaultType());
clientIdentity.load(null, null);
char[] passphrase = "client-secret".toCharArray();

KeystoreService keystoreService = EasyMock.createNiceMock(KeystoreService.class);
EasyMock.expect(keystoreService.getKeystoreForHttpClient()).andReturn(clientIdentity).anyTimes();
AliasService aliasService = EasyMock.createNiceMock(AliasService.class);
EasyMock.expect(aliasService.getHttpClientKeyPassphrase()).andReturn(passphrase).anyTimes();

GatewayServices services = EasyMock.createNiceMock(GatewayServices.class);
EasyMock.expect(services.getService(ServiceType.KEYSTORE_SERVICE)).andReturn(keystoreService).anyTimes();
EasyMock.expect(services.getService(ServiceType.ALIAS_SERVICE)).andReturn(aliasService).anyTimes();
EasyMock.replay(gatewayConfig, keystoreService, aliasService, services);

GatewayWebsocketHandler handler = new GatewayWebsocketHandler(gatewayConfig, services);
Map<String, Object> props = new HashMap<>();
handler.configureClientIdentity(props);

Assert.assertSame(clientIdentity, props.get(GatewayWebsocketHandler.KEYSTORE_USER_PROPERTY));
Assert.assertSame(passphrase, props.get(GatewayWebsocketHandler.KEYSTORE_KEY_PASSPHRASE_USER_PROPERTY));
}

@Test
public void testConfigureClientIdentityDisabledWhenNotTwoWaySsl() throws Exception {
GatewayConfig gatewayConfig = EasyMock.createNiceMock(GatewayConfig.class);
EasyMock.expect(gatewayConfig.isHttpClientTwoWaySslEnabled()).andReturn(false).anyTimes();
GatewayServices services = EasyMock.createNiceMock(GatewayServices.class);
EasyMock.replay(gatewayConfig, services);

GatewayWebsocketHandler handler = new GatewayWebsocketHandler(gatewayConfig, services);
Map<String, Object> props = new HashMap<>();
handler.configureClientIdentity(props);

Assert.assertFalse(props.containsKey(GatewayWebsocketHandler.KEYSTORE_USER_PROPERTY));
Assert.assertFalse(props.containsKey(GatewayWebsocketHandler.KEYSTORE_KEY_PASSPHRASE_USER_PROPERTY));
}

@Test
public void testConfigureClientIdentityTwoWaySslNullKeystoreContributesNothing() throws Exception {
GatewayConfig gatewayConfig = EasyMock.createNiceMock(GatewayConfig.class);
EasyMock.expect(gatewayConfig.isHttpClientTwoWaySslEnabled()).andReturn(true).anyTimes();
EasyMock.expect(gatewayConfig.isSingleEkuEnabled()).andReturn(false).anyTimes();

KeystoreService keystoreService = EasyMock.createNiceMock(KeystoreService.class);
EasyMock.expect(keystoreService.getKeystoreForGateway()).andReturn(null).anyTimes();
AliasService aliasService = EasyMock.createNiceMock(AliasService.class);

GatewayServices services = EasyMock.createNiceMock(GatewayServices.class);
EasyMock.expect(services.getService(ServiceType.KEYSTORE_SERVICE)).andReturn(keystoreService).anyTimes();
EasyMock.expect(services.getService(ServiceType.ALIAS_SERVICE)).andReturn(aliasService).anyTimes();
EasyMock.replay(gatewayConfig, keystoreService, aliasService, services);

GatewayWebsocketHandler handler = new GatewayWebsocketHandler(gatewayConfig, services);
Map<String, Object> props = new HashMap<>();
handler.configureClientIdentity(props);

Assert.assertFalse(props.containsKey(GatewayWebsocketHandler.KEYSTORE_USER_PROPERTY));
Assert.assertFalse(props.containsKey(GatewayWebsocketHandler.KEYSTORE_KEY_PASSPHRASE_USER_PROPERTY));
}

private ServletUpgradeRequest createServletUpgradeRequest(String url) throws Exception {
HttpServletRequest mockRequest = new org.apache.knox.test.mock.MockHttpServletRequest() {
@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.knox.gateway.websockets;

import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.junit.Assert;
import org.junit.Test;

import javax.websocket.ClientEndpointConfig;
import java.security.KeyStore;

public class ProxyWebSocketAdapterTest {

private static KeyStore emptyKeyStore() throws Exception {
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null, null);
return keyStore;
}

@Test
public void testConfigureSslAppliesKeystoreAndTruststore() throws Exception {
KeyStore identity = emptyKeyStore();
KeyStore truststore = emptyKeyStore();
ClientEndpointConfig clientConfig = ClientEndpointConfig.Builder.create().build();
clientConfig.getUserProperties().put(GatewayWebsocketHandler.TRUSTSTORE_USER_PROPERTY, truststore);
clientConfig.getUserProperties().put(GatewayWebsocketHandler.KEYSTORE_USER_PROPERTY, identity);
clientConfig.getUserProperties().put(GatewayWebsocketHandler.KEYSTORE_KEY_PASSPHRASE_USER_PROPERTY, "secret".toCharArray());

SslContextFactory sslContextFactory = new SslContextFactory.Client();
ProxyWebSocketAdapter.configureSsl(sslContextFactory, clientConfig);

Assert.assertSame(identity, sslContextFactory.getKeyStore());
Assert.assertSame(truststore, sslContextFactory.getTrustStore());
}

@Test
public void testConfigureSslNoKeystoreWhenAbsent() throws Exception {
KeyStore truststore = emptyKeyStore();
ClientEndpointConfig clientConfig = ClientEndpointConfig.Builder.create().build();
clientConfig.getUserProperties().put(GatewayWebsocketHandler.TRUSTSTORE_USER_PROPERTY, truststore);

SslContextFactory sslContextFactory = new SslContextFactory.Client();
ProxyWebSocketAdapter.configureSsl(sslContextFactory, clientConfig);

Assert.assertNull(sslContextFactory.getKeyStore());
Assert.assertSame(truststore, sslContextFactory.getTrustStore());
}

@Test
public void testConfigureSslKeystorePresentNullPassphrase() throws Exception {
KeyStore identity = emptyKeyStore();
KeyStore truststore = emptyKeyStore();
ClientEndpointConfig clientConfig = ClientEndpointConfig.Builder.create().build();
clientConfig.getUserProperties().put(GatewayWebsocketHandler.TRUSTSTORE_USER_PROPERTY, truststore);
clientConfig.getUserProperties().put(GatewayWebsocketHandler.KEYSTORE_USER_PROPERTY, identity);

SslContextFactory sslContextFactory = new SslContextFactory.Client();
ProxyWebSocketAdapter.configureSsl(sslContextFactory, clientConfig);

Assert.assertSame(identity, sslContextFactory.getKeyStore());
Assert.assertSame(truststore, sslContextFactory.getTrustStore());
}
}
Loading