blob: cc8d3ed3d29f007c52cbd4d869df17396cc86f7b [file] [log] [blame]
/*
* 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.qpid.protonj2.client.transport.netty4;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import java.io.IOException;
import java.net.URISyntaxException;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.apache.qpid.protonj2.client.SslOptions;
import org.apache.qpid.protonj2.client.transport.Transport;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Test basic functionality of the Netty based TCP Transport running in secure mode (SSL).
*/
@Timeout(30)
public class SslTransportTest extends TcpTransportTest {
private static final Logger LOG = LoggerFactory.getLogger(SslTransportTest.class);
public static final String PASSWORD = "password";
public static final String SERVER_KEYSTORE = "src/test/resources/broker-jks.keystore";
public static final String SERVER_TRUSTSTORE = "src/test/resources/broker-jks.truststore";
public static final String SERVER_WRONG_HOST_KEYSTORE = "src/test/resources/broker-wrong-host-jks.keystore";
public static final String CLIENT_KEYSTORE = "src/test/resources/client-jks.keystore";
public static final String CLIENT_MULTI_KEYSTORE = "src/test/resources/client-multiple-keys-jks.keystore";
public static final String CLIENT_TRUSTSTORE = "src/test/resources/client-jks.truststore";
public static final String OTHER_CA_TRUSTSTORE = "src/test/resources/other-ca-jks.truststore";
public static final String SERVER_CLASSPATH_KEYSTORE = "classpath:broker-jks.keystore";
public static final String SERVER_CLASSPATH_TRUSTSTORE = "classpath:broker-jks.truststore";
public static final String CLIENT_KEY_ALIAS = "client";
public static final String CLIENT_DN = "O=Client,CN=client";
public static final String CLIENT2_KEY_ALIAS = "client2";
public static final String CLIENT2_DN = "O=Client2,CN=client2";
public static final String KEYSTORE_TYPE = "jks";
@Test
public void testConnectToServerWithoutTrustStoreFails() throws Exception {
try (NettyEchoServer server = createEchoServer()) {
server.start();
final int port = server.getServerPort();
Transport transport = createTransport(createTransportOptions(), createSSLOptionsWithoutTrustStore(false));
try {
transport.connect(HOSTNAME, port, testListener).awaitConnect();
fail("Should have failed to connect to the server: " + HOSTNAME + ":" + port);
} catch (Exception e) {
LOG.info("Connection failed to untrusted test server: {}:{}", HOSTNAME, port);
}
assertFalse(transport.isConnected());
transport.close();
}
logTransportErrors();
assertFalse(exceptions.isEmpty());
}
@Test
public void testConnectToServerUsingUntrustedKeyFails() throws Exception {
try (NettyEchoServer server = createEchoServer()) {
server.start();
final int port = server.getServerPort();
SslOptions sslOptions = createSSLOptions();
sslOptions.trustStoreLocation(OTHER_CA_TRUSTSTORE);
sslOptions.trustStorePassword(PASSWORD);
Transport transport = createTransport(createTransportOptions(), sslOptions);
try {
transport.connect(HOSTNAME, port, testListener).awaitConnect();
fail("Should have failed to connect to the server: " + HOSTNAME + ":" + port);
} catch (Exception e) {
LOG.info("Connection failed to untrusted test server: {}:{}", HOSTNAME, port);
}
assertFalse(transport.isConnected());
transport.close();
}
}
@Test
public void testConnectToServerWithWrongKeyStorePasswordFails() throws Exception {
try (NettyEchoServer server = createEchoServer()) {
server.start();
final int port = server.getServerPort();
final CountDownLatch errored = new CountDownLatch(1);
final SslOptions sslOptions = createSSLOptions();
sslOptions.keyStoreLocation(CLIENT_KEYSTORE);
sslOptions.keyStorePassword("wrong");
Transport transport = createTransport(createTransportOptions(), sslOptions);
transport.connect(HOSTNAME, port, new NettyTransportListener() {
@Override
public void transportError(Throwable cause) {
LOG.info("Transport error caught: {}", cause.getMessage(), cause);
errored.countDown();
}
});
assertTrue(errored.await(5, TimeUnit.SECONDS));
try {
transport.awaitConnect();
fail("Should have failed to connect to the server: " + HOSTNAME + ":" + port);
} catch (Exception e) {
LOG.info("Connection failed when key store password was incorrect: {}:{}", HOSTNAME, port);
}
assertFalse(transport.isConnected());
transport.close();
}
}
@Test
public void testConnectToServerWithWrongTrustStorePasswordFails() throws Exception {
try (NettyEchoServer server = createEchoServer()) {
server.start();
final int port = server.getServerPort();
final CountDownLatch errored = new CountDownLatch(1);
final SslOptions sslOptions = createSSLOptions();
sslOptions.trustStoreLocation(CLIENT_TRUSTSTORE);
sslOptions.trustStorePassword("wrong");
Transport transport = createTransport(createTransportOptions(), sslOptions);
transport.connect(HOSTNAME, port, new NettyTransportListener() {
@Override
public void transportError(Throwable cause) {
LOG.info("Transport error caught: {}", cause.getMessage(), cause);
errored.countDown();
}
});
assertTrue(errored.await(5, TimeUnit.SECONDS));
try {
transport.awaitConnect();
fail("Should have failed to connect to the server: " + HOSTNAME + ":" + port);
} catch (Exception e) {
LOG.info("Connection failed when trust store password was incorrect: {}:{}", HOSTNAME, port);
}
assertFalse(transport.isConnected());
transport.close();
}
}
@Test
public void testConnectToServerClientTrustsAll() throws Exception {
try (NettyEchoServer server = createEchoServer()) {
server.start();
final int port = server.getServerPort();
Transport transport = createTransport(createTransportOptions(), createSSLOptionsWithoutTrustStore(true));
try {
transport.connect(HOSTNAME, port, testListener).awaitConnect();
LOG.info("Connection established to test server: {}:{}", HOSTNAME, port);
} catch (Exception e) {
fail("Should not have failed to connect to the server at " + HOSTNAME + ":" + port + " but got exception: " + e);
}
assertTrue(transport.isConnected());
assertTrue(transport.isSecure());
transport.close();
}
logTransportErrors();
assertTrue(exceptions.isEmpty());
}
@Test
public void testConnectToServerWithServerClasspathStores() throws Exception {
try (NettyEchoServer server = createEchoServer()) {
server.start();
final int port = server.getServerPort();
Transport transport = createTransport(createTransportOptions(), createServerClasspathSSLOptions());
try {
transport.connect(HOSTNAME, port, testListener).awaitConnect();
LOG.info("Connection established to test server: {}:{}", HOSTNAME, port);
} catch (Exception e) {
fail("Should not have failed to connect to the server at " + HOSTNAME + ":" + port + " but got exception: " + e);
}
assertTrue(transport.isConnected());
assertTrue(transport.isSecure());
transport.close();
}
logTransportErrors();
assertTrue(exceptions.isEmpty());
}
@Test
public void testConnectWithNeedClientAuth() throws Exception {
try (NettyEchoServer server = createEchoServer(true)) {
server.start();
final int port = server.getServerPort();
Transport transport = createTransport(createTransportOptions(), createSSLOptions());
try {
transport.connect(HOSTNAME, port, testListener).awaitConnect();
LOG.info("Connection established to test server: {}:{}", HOSTNAME, port);
} catch (Exception e) {
fail("Should not have failed to connect to the server at " + HOSTNAME + ":" + port + " but got exception: " + e);
}
assertTrue(transport.isConnected());
assertTrue(transport.isSecure());
// Verify there was a certificate sent to the server
assertTrue(server.getSslHandler().handshakeFuture().await(2, TimeUnit.SECONDS), "Server handshake did not complete in allotted time");
assertNotNull(server.getSslHandler().engine().getSession().getPeerCertificates());
transport.close();
}
logTransportErrors();
assertTrue(exceptions.isEmpty());
}
@Test
public void testConnectWithSpecificClientAuthKeyAlias1() throws Exception {
doClientAuthAliasTestImpl(CLIENT_KEY_ALIAS, CLIENT_DN);
}
@Test
public void testConnectWithSpecificClientAuthKeyAlias2() throws Exception {
doClientAuthAliasTestImpl(CLIENT2_KEY_ALIAS, CLIENT2_DN);
}
private void doClientAuthAliasTestImpl(String alias, String expectedDN) throws Exception, URISyntaxException, IOException, InterruptedException {
try (NettyEchoServer server = createEchoServer(true)) {
server.start();
final int port = server.getServerPort();
SslOptions sslOptions = createSSLOptions();
sslOptions.keyStoreLocation(CLIENT_MULTI_KEYSTORE);
sslOptions.keyAlias(alias);
Transport transport = createTransport(createTransportOptions(), sslOptions);
try {
transport.connect(HOSTNAME, port, testListener).awaitConnect();
LOG.info("Connection established to test server: {}:{}", HOSTNAME, port);
} catch (Exception e) {
fail("Should not have failed to connect to the server at " + HOSTNAME + ":" + port + " but got exception: " + e);
}
assertTrue(transport.isConnected());
assertTrue(transport.isSecure());
assertTrue(server.getSslHandler().handshakeFuture().await(2, TimeUnit.SECONDS), "Server handshake did not complete in allotted time");
Certificate[] peerCertificates = server.getSslHandler().engine().getSession().getPeerCertificates();
assertNotNull(peerCertificates);
Certificate cert = peerCertificates[0];
assertTrue(cert instanceof X509Certificate);
String dn = ((X509Certificate)cert).getSubjectX500Principal().getName();
assertEquals(expectedDN, dn, "Unexpected certificate DN");
transport.close();
}
logTransportErrors();
assertTrue(exceptions.isEmpty());
}
@Test
public void testConnectToServerVerifyHost() throws Exception {
doConnectToServerVerifyHostTestImpl(true);
}
@Test
public void testConnectToServerNoVerifyHost() throws Exception {
doConnectToServerVerifyHostTestImpl(false);
}
private void doConnectToServerVerifyHostTestImpl(boolean verifyHost) throws Exception, URISyntaxException, IOException, InterruptedException {
SslOptions serverOptions = createServerSSLOptions();
serverOptions.keyStoreLocation(SERVER_WRONG_HOST_KEYSTORE);
try (NettyEchoServer server = createEchoServer(serverOptions)) {
server.start();
final int port = server.getServerPort();
SslOptions clientOptions = createSSLOptionsIsVerify(verifyHost);
if (verifyHost) {
assertTrue(clientOptions.verifyHost(), "Expected verifyHost to be true");
} else {
assertFalse(clientOptions.verifyHost(), "Expected verifyHost to be false");
}
Transport transport = createTransport(createTransportOptions(), clientOptions);
try {
transport.connect(HOSTNAME, port, testListener).awaitConnect();
if (verifyHost) {
fail("Should not have connected to the server: " + HOSTNAME + ":" + port);
}
} catch (Exception e) {
if (verifyHost) {
LOG.info("Connection failed to test server: {}:{} as expected.", HOSTNAME, port);
} else {
LOG.error("Failed to connect to test server: {}:{}" + HOSTNAME, port, e);
fail("Should have connected to the server at " + HOSTNAME + ":" + port + " but got exception: " + e);
}
}
assertNotNull(transport.getSslOptions());
assertEquals(verifyHost, transport.getSslOptions().verifyHost());
if (verifyHost) {
assertFalse(transport.isConnected());
} else {
assertTrue(transport.isConnected());
}
transport.close();
}
}
@Override
protected SslOptions createSSLOptions() {
return createSSLOptionsIsVerify(false);
}
protected SslOptions createSSLOptionsIsVerify(boolean verifyHost) {
SslOptions options = new SslOptions();
options.sslEnabled(true);
options.keyStoreLocation(CLIENT_KEYSTORE);
options.keyStorePassword(PASSWORD);
options.trustStoreLocation(CLIENT_TRUSTSTORE);
options.trustStorePassword(PASSWORD);
options.storeType(KEYSTORE_TYPE);
options.verifyHost(verifyHost);
return options;
}
protected SslOptions createSSLOptionsWithoutTrustStore(boolean trustAll) {
SslOptions options = new SslOptions();
options.sslEnabled(true);
options.storeType(KEYSTORE_TYPE);
options.trustAll(trustAll);
return options;
}
@Override
protected SslOptions createServerSSLOptions() {
SslOptions options = new SslOptions();
// Run the server in JDK mode for now to validate cross compatibility
options.sslEnabled(true);
options.keyStoreLocation(SERVER_KEYSTORE);
options.keyStorePassword(PASSWORD);
options.trustStoreLocation(SERVER_TRUSTSTORE);
options.trustStorePassword(PASSWORD);
options.storeType(KEYSTORE_TYPE);
options.verifyHost(false);
return options;
}
protected SslOptions createServerClasspathSSLOptions() {
SslOptions options = new SslOptions();
// Run the server in JDK mode for now to validate cross compatibility
options.sslEnabled(true);
options.keyStoreLocation(SERVER_CLASSPATH_KEYSTORE);
options.keyStorePassword(PASSWORD);
options.trustStoreLocation(SERVER_CLASSPATH_TRUSTSTORE);
options.trustStorePassword(PASSWORD);
options.storeType(KEYSTORE_TYPE);
options.verifyHost(false);
return options;
}
}