blob: 71d5e3ccd80fcef39bf8ccf1cccf56c7d78d9287 [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.systests.jms_1_1.extensions.tls;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.apache.qpid.test.utils.TestSSLConstants.JAVA_KEYSTORE_TYPE;
import static org.apache.qpid.test.utils.TestSSLConstants.BROKER_KEYSTORE_PASSWORD;
import static org.apache.qpid.test.utils.TestSSLConstants.BROKER_TRUSTSTORE_PASSWORD;
import static org.apache.qpid.test.utils.TestSSLConstants.KEYSTORE_PASSWORD;
import static org.apache.qpid.test.utils.TestSSLConstants.TRUSTSTORE_PASSWORD;
import static org.hamcrest.CoreMatchers.anyOf;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeThat;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.security.Key;
import java.security.cert.Certificate;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.jms.Connection;
import javax.jms.JMSException;
import javax.jms.Session;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.apache.qpid.server.model.Port;
import org.apache.qpid.server.model.Protocol;
import org.apache.qpid.server.security.FileKeyStore;
import org.apache.qpid.server.security.FileTrustStore;
import org.apache.qpid.systests.AmqpManagementFacade;
import org.apache.qpid.systests.ConnectionBuilder;
import org.apache.qpid.systests.JmsTestBase;
import org.apache.qpid.test.utils.TestSSLConstants;
import org.apache.qpid.test.utils.TestSSLUtils;
import org.apache.qpid.tests.utils.BrokerAdmin;
public class TlsTest extends JmsTestBase
{
public static final String TEST_PROFILE_RESOURCE_BASE = System.getProperty("java.io.tmpdir") + "/";
public static final String BROKER_KEYSTORE =
TEST_PROFILE_RESOURCE_BASE + org.apache.qpid.test.utils.TestSSLConstants.BROKER_KEYSTORE;
public static final String BROKER_TRUSTSTORE =
TEST_PROFILE_RESOURCE_BASE + org.apache.qpid.test.utils.TestSSLConstants.BROKER_TRUSTSTORE;
public static final String KEYSTORE =
TEST_PROFILE_RESOURCE_BASE + org.apache.qpid.test.utils.TestSSLConstants.KEYSTORE;
public static final String TRUSTSTORE =
TEST_PROFILE_RESOURCE_BASE + org.apache.qpid.test.utils.TestSSLConstants.TRUSTSTORE;
@BeforeClass
public static void setUp() throws Exception
{
System.setProperty("javax.net.debug", "ssl");
// workaround for QPID-8069
if (getProtocol() != Protocol.AMQP_1_0 && getProtocol() != Protocol.AMQP_0_10)
{
System.setProperty("amqj.MaximumStateWait", "4000");
}
// legacy client keystore/truststore types can only be configured with JVM settings
if (getProtocol() != Protocol.AMQP_1_0)
{
System.setProperty("javax.net.ssl.trustStoreType", JAVA_KEYSTORE_TYPE);
System.setProperty("javax.net.ssl.keyStoreType", JAVA_KEYSTORE_TYPE);
}
}
@AfterClass
public static void tearDown() throws Exception
{
System.clearProperty("javax.net.debug");
if (getProtocol() != Protocol.AMQP_1_0)
{
System.clearProperty("amqj.MaximumStateWait");
}
if (getProtocol() != Protocol.AMQP_1_0)
{
System.clearProperty("javax.net.ssl.trustStoreType");
System.clearProperty("javax.net.ssl.keyStoreType");
}
}
@Test
public void testCreateSSLConnectionUsingConnectionURLParams() throws Exception
{
//Start the broker (NEEDing client certificate authentication)
int port = configureTlsPort(getTestPortName(), true, false, false);
InetSocketAddress brokerAddress = getBrokerAdmin().getBrokerAddress(BrokerAdmin.PortType.AMQP);
Connection connection = getConnectionBuilder().setSslPort(port)
.setHost(brokerAddress.getHostName())
.setTls(true)
.setKeyStoreLocation(KEYSTORE)
.setKeyStorePassword(KEYSTORE_PASSWORD)
.setTrustStoreLocation(TRUSTSTORE)
.setTrustStorePassword(TRUSTSTORE_PASSWORD)
.build();
try
{
assertConnection(connection);
}
finally
{
connection.close();
}
}
@Test
public void testCreateSSLConnectionWithCertificateTrust() throws Exception
{
assumeThat("Qpid JMS Client does not support trusting of a certificate",
getProtocol(),
is(not(equalTo(Protocol.AMQP_1_0))));
int port = configureTlsPort(getTestPortName(), false, false, false);
File trustCertFile = extractCertFileFromTestTrustStore();
InetSocketAddress brokerAddress = getBrokerAdmin().getBrokerAddress(BrokerAdmin.PortType.AMQP);
Connection connection = getConnectionBuilder().setSslPort(port)
.setHost(brokerAddress.getHostName())
.setTls(true)
.setOptions(Collections.singletonMap("trusted_certs_path",
encodePathOption(trustCertFile.getCanonicalPath())))
.build();
try
{
assertConnection(connection);
}
finally
{
connection.close();
}
}
@Test
public void testSSLConnectionToPlainPortRejected() throws Exception
{
assumeThat("QPID-8069", getProtocol(), is(anyOf(equalTo(Protocol.AMQP_1_0), equalTo(Protocol.AMQP_0_10))));
setSslStoreSystemProperties();
try
{
InetSocketAddress brokerAddress = getBrokerAdmin().getBrokerAddress(BrokerAdmin.PortType.AMQP);
getConnectionBuilder().setSslPort(brokerAddress.getPort())
.setHost(brokerAddress.getHostName())
.setTls(true)
.build();
fail("Exception not thrown");
}
catch (JMSException e)
{
// PASS
}
finally
{
clearSslStoreSystemProperties();
}
}
@Test
public void testHostVerificationIsOnByDefault() throws Exception
{
assumeThat("QPID-8069", getProtocol(), is(anyOf(equalTo(Protocol.AMQP_1_0), equalTo(Protocol.AMQP_0_10))));
//Start the broker (NEEDing client certificate authentication)
int port = configureTlsPort(getTestPortName(), true, false, false);
try
{
getConnectionBuilder().setSslPort(port)
.setHost("127.0.0.1")
.setTls(true)
.setKeyStoreLocation(KEYSTORE)
.setKeyStorePassword(KEYSTORE_PASSWORD)
.setTrustStoreLocation(TRUSTSTORE)
.setTrustStorePassword(TRUSTSTORE_PASSWORD)
.build();
fail("Exception not thrown");
}
catch (JMSException e)
{
// PASS
}
Connection connection = getConnectionBuilder().setSslPort(port)
.setHost("127.0.0.1")
.setTls(true)
.setKeyStoreLocation(KEYSTORE)
.setKeyStorePassword(KEYSTORE_PASSWORD)
.setTrustStoreLocation(TRUSTSTORE)
.setTrustStorePassword(TRUSTSTORE_PASSWORD)
.setVerifyHostName(false)
.build();
try
{
assertConnection(connection);
}
finally
{
connection.close();
}
}
@Test
public void testCreateSslConnectionUsingJVMSettings() throws Exception
{
//Start the broker (NEEDing client certificate authentication)
int port = configureTlsPort(getTestPortName(), true, false, false);
setSslStoreSystemProperties();
try
{
Connection connection = getConnectionBuilder().setSslPort(port)
.setTls(true)
.build();
try
{
assertConnection(connection);
}
finally
{
connection.close();
}
}
finally
{
clearSslStoreSystemProperties();
}
}
@Test
public void testMultipleCertsInSingleStore() throws Exception
{
//Start the broker (NEEDing client certificate authentication)
int port = configureTlsPort(getTestPortName(), true, false, false);
setSslStoreSystemProperties();
try
{
Connection connection = getConnectionBuilder().setClientId(getTestName())
.setSslPort(port)
.setTls(true)
.setKeyAlias(TestSSLConstants.CERT_ALIAS_APP1)
.build();
try
{
assertConnection(connection);
}
finally
{
connection.close();
}
Connection connection2 = getConnectionBuilder().setSslPort(port)
.setTls(true)
.setKeyAlias(TestSSLConstants.CERT_ALIAS_APP2)
.build();
try
{
assertConnection(connection2);
}
finally
{
connection2.close();
}
}
finally
{
clearSslStoreSystemProperties();
}
}
@Test
public void testVerifyHostNameWithIncorrectHostname() throws Exception
{
assumeThat("QPID-8069", getProtocol(), is(anyOf(equalTo(Protocol.AMQP_1_0), equalTo(Protocol.AMQP_0_10))));
//Start the broker (WANTing client certificate authentication)
int port = configureTlsPort(getTestPortName(), false, true, false);
setSslStoreSystemProperties();
try
{
getConnectionBuilder().setSslPort(port)
.setHost("127.0.0.1")
.setTls(true)
.setVerifyHostName(true)
.build();
fail("Exception not thrown");
}
catch (JMSException e)
{
// PASS
}
finally
{
clearSslStoreSystemProperties();
}
}
@Test
public void testVerifyLocalHost() throws Exception
{
//Start the broker (WANTing client certificate authentication)
int port = configureTlsPort(getTestPortName(), false, true, false);
setSslStoreSystemProperties();
try
{
Connection connection = getConnectionBuilder().setSslPort(port)
.setHost("localhost")
.setTls(true)
.build();
try
{
assertConnection(connection);
}
finally
{
connection.close();
}
}
finally
{
clearSslStoreSystemProperties();
}
}
@Test
public void testCreateSSLConnectionUsingConnectionURLParamsTrustStoreOnly() throws Exception
{
//Start the broker (WANTing client certificate authentication)
int port = configureTlsPort(getTestPortName(), false, true, false);
InetSocketAddress brokerAddress = getBrokerAdmin().getBrokerAddress(BrokerAdmin.PortType.AMQP);
Connection connection = getConnectionBuilder().setSslPort(port)
.setHost(brokerAddress.getHostName())
.setTls(true)
.setTrustStoreLocation(TRUSTSTORE)
.setTrustStorePassword(TRUSTSTORE_PASSWORD)
.build();
try
{
assertConnection(connection);
}
finally
{
connection.close();
}
}
@Test
public void testClientCertificateMissingWhilstNeeding() throws Exception
{
assumeThat("QPID-8069", getProtocol(), is(anyOf(equalTo(Protocol.AMQP_1_0), equalTo(Protocol.AMQP_0_10))));
//Start the broker (NEEDing client certificate authentication)
int port = configureTlsPort(getTestPortName(), true, false, false);
try
{
getConnectionBuilder().setSslPort(port)
.setHost(getBrokerAdmin().getBrokerAddress(BrokerAdmin.PortType.AMQP).getHostName())
.setTls(true)
.setTrustStoreLocation(TRUSTSTORE)
.setTrustStorePassword(TRUSTSTORE_PASSWORD)
.build();
fail("Connection was established successfully");
}
catch (JMSException e)
{
// PASS
}
}
@Test
public void testClientCertificateMissingWhilstWanting() throws Exception
{
//Start the broker (WANTing client certificate authentication)
int port = configureTlsPort(getTestPortName(), false, true, false);
InetSocketAddress brokerAddress = getBrokerAdmin().getBrokerAddress(BrokerAdmin.PortType.AMQP);
Connection connection = getConnectionBuilder().setSslPort(port)
.setHost(brokerAddress.getHostName())
.setTls(true)
.setTrustStoreLocation(TRUSTSTORE)
.setTrustStorePassword(TRUSTSTORE_PASSWORD)
.build();
try
{
assertConnection(connection);
}
finally
{
connection.close();
}
}
@Test
public void testClientCertMissingWhilstWantingAndNeeding() throws Exception
{
assumeThat("QPID-8069", getProtocol(), is(anyOf(equalTo(Protocol.AMQP_1_0), equalTo(Protocol.AMQP_0_10))));
//Start the broker (NEEDing and WANTing client certificate authentication)
int port = configureTlsPort(getTestPortName(), true, true, false);
try
{
getConnectionBuilder().setSslPort(port)
.setHost(getBrokerAdmin().getBrokerAddress(BrokerAdmin.PortType.AMQP).getHostName())
.setTls(true)
.setTrustStoreLocation(TRUSTSTORE)
.setTrustStorePassword(TRUSTSTORE_PASSWORD)
.build();
fail("Connection was established successfully");
}
catch (JMSException e)
{
// PASS
}
}
@Test
public void testCreateSSLandTCPonSamePort() throws Exception
{
//Start the broker (WANTing client certificate authentication)
int port = configureTlsPort(getTestPortName(), false, true, true);
InetSocketAddress brokerAddress = getBrokerAdmin().getBrokerAddress(BrokerAdmin.PortType.AMQP);
Connection connection = getConnectionBuilder().setSslPort(port)
.setHost(brokerAddress.getHostName())
.setTls(true)
.setKeyStoreLocation(KEYSTORE)
.setKeyStorePassword(KEYSTORE_PASSWORD)
.setTrustStoreLocation(TRUSTSTORE)
.setTrustStorePassword(TRUSTSTORE_PASSWORD)
.build();
try
{
assertConnection(connection);
}
finally
{
connection.close();
}
Connection connection2 = getConnectionBuilder().setPort(port)
.setHost(brokerAddress.getHostName())
.build();
try
{
assertConnection(connection2);
}
finally
{
connection2.close();
}
}
@Test
public void testCreateSSLWithCertFileAndPrivateKey() throws Exception
{
assumeThat("Qpid JMS Client does not support trusting of a certificate",
getProtocol(),
is(not(equalTo(Protocol.AMQP_1_0))));
assumeThat("QPID-8255: certificate can only be loaded using jks keystore ",
java.security.KeyStore.getDefaultType(),
is(equalTo("jks")));
//Start the broker (NEEDing client certificate authentication)
int port = configureTlsPort(getTestPortName(), true, false, false);
clearSslStoreSystemProperties();
File[] certAndKeyFiles = extractResourcesFromTestKeyStore();
final Map<String, String> options = new HashMap<>();
options.put("client_cert_path", encodePathOption(certAndKeyFiles[1].getCanonicalPath()));
options.put("client_cert_priv_key_path", encodePathOption(certAndKeyFiles[0].getCanonicalPath()));
InetSocketAddress brokerAddress = getBrokerAdmin().getBrokerAddress(BrokerAdmin.PortType.AMQP);
Connection connection = getConnectionBuilder().setSslPort(port)
.setHost(brokerAddress.getHostName())
.setTls(true)
.setTrustStoreLocation(TRUSTSTORE)
.setTrustStorePassword(TRUSTSTORE_PASSWORD)
.setVerifyHostName(false)
.setOptions(options)
.build();
try
{
assertConnection(connection);
}
finally
{
connection.close();
}
}
private int configureTlsPort(final String portName,
final boolean needClientAuth,
final boolean wantClientAuth,
final boolean samePort) throws Exception
{
return createTlsPort(portName,
needClientAuth,
wantClientAuth,
samePort,
getConnectionBuilder(),
new AmqpManagementFacade(getProtocol()),
getBrokerAdmin().getBrokerAddress(BrokerAdmin.PortType.AMQP).getPort());
}
public static int createTlsPort(final String portName,
final boolean needClientAuth,
final boolean wantClientAuth,
final boolean plainAndSsl,
final ConnectionBuilder connectionBuilder,
final AmqpManagementFacade managementFacade,
final int brokerPort) throws Exception
{
Connection connection = connectionBuilder.setVirtualHost("$management").build();
try
{
connection.start();
String keyStoreName = portName + "KeyStore";
String trustStoreName = portName + "TrustStore";
String authenticationProvider = null;
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
try
{
List<Map<String, Object>> ports =
managementFacade.managementQueryObjects(session, "org.apache.qpid.AmqpPort");
for (Map<String, Object> port : ports)
{
String name = String.valueOf(port.get(Port.NAME));
Session s = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
try
{
Map<String, Object> attributes = managementFacade.readEntityUsingAmqpManagement(s,
"org.apache.qpid.AmqpPort",
name,
false);
if (attributes.get("boundPort").equals(brokerPort))
{
authenticationProvider = String.valueOf(attributes.get(Port.AUTHENTICATION_PROVIDER));
break;
}
}
finally
{
s.close();
}
}
}
finally
{
session.close();
}
session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
try
{
final Map<String, Object> keyStoreAttributes = new HashMap<>();
keyStoreAttributes.put("storeUrl", BROKER_KEYSTORE);
keyStoreAttributes.put("password", BROKER_KEYSTORE_PASSWORD);
keyStoreAttributes.put("keyStoreType", JAVA_KEYSTORE_TYPE);
managementFacade.createEntityAndAssertResponse(keyStoreName,
FileKeyStore.class.getName(),
keyStoreAttributes,
session);
}
finally
{
session.close();
}
session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
try
{
final Map<String, Object> trustStoreAttributes = new HashMap<>();
trustStoreAttributes.put("storeUrl", BROKER_TRUSTSTORE);
trustStoreAttributes.put("password", BROKER_TRUSTSTORE_PASSWORD);
trustStoreAttributes.put("trustStoreType", JAVA_KEYSTORE_TYPE);
managementFacade.createEntityAndAssertResponse(trustStoreName,
FileTrustStore.class.getName(),
trustStoreAttributes,
session);
}
finally
{
session.close();
}
session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
try
{
Map<String, Object> sslPortAttributes = new HashMap<>();
sslPortAttributes.put(Port.TRANSPORTS, plainAndSsl ? "[\"SSL\",\"TCP\"]" : "[\"SSL\"]");
sslPortAttributes.put(Port.PORT, 0);
sslPortAttributes.put(Port.AUTHENTICATION_PROVIDER, authenticationProvider);
sslPortAttributes.put(Port.NEED_CLIENT_AUTH, needClientAuth);
sslPortAttributes.put(Port.WANT_CLIENT_AUTH, wantClientAuth);
sslPortAttributes.put(Port.NAME, portName);
sslPortAttributes.put(Port.KEY_STORE, keyStoreName);
sslPortAttributes.put(Port.TRUST_STORES, "[\"" + trustStoreName + "\"]");
managementFacade.createEntityAndAssertResponse(portName,
"org.apache.qpid.AmqpPort",
sslPortAttributes,
session);
}
finally
{
session.close();
}
session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
try
{
Map<String, Object> portEffectiveAttributes =
managementFacade.readEntityUsingAmqpManagement(session,
"org.apache.qpid.AmqpPort",
portName,
false);
if (portEffectiveAttributes.containsKey("boundPort"))
{
return (int) portEffectiveAttributes.get("boundPort");
}
throw new RuntimeException("Bound port is not found");
}
finally
{
session.close();
}
}
finally
{
connection.close();
}
}
private void setSslStoreSystemProperties()
{
System.setProperty("javax.net.ssl.keyStore", KEYSTORE);
System.setProperty("javax.net.ssl.keyStorePassword", KEYSTORE_PASSWORD);
System.setProperty("javax.net.ssl.trustStore", TRUSTSTORE);
System.setProperty("javax.net.ssl.trustStorePassword", TRUSTSTORE_PASSWORD);
}
private void clearSslStoreSystemProperties()
{
System.clearProperty("javax.net.ssl.keyStore");
System.clearProperty("javax.net.ssl.keyStorePassword");
System.clearProperty("javax.net.ssl.trustStore");
System.clearProperty("javax.net.ssl.trustStorePassword");
}
private File[] extractResourcesFromTestKeyStore() throws Exception
{
java.security.KeyStore ks = java.security.KeyStore.getInstance(JAVA_KEYSTORE_TYPE);
try (InputStream is = new FileInputStream(KEYSTORE))
{
ks.load(is, KEYSTORE_PASSWORD.toCharArray());
}
File privateKeyFile = Files.createTempFile(getTestName(), ".private-key.der").toFile();
try (FileOutputStream kos = new FileOutputStream(privateKeyFile))
{
Key pvt = ks.getKey(TestSSLConstants.CERT_ALIAS_APP1, KEYSTORE_PASSWORD.toCharArray());
kos.write(TestSSLUtils.privateKeyToPEM(pvt).getBytes(UTF_8));
}
File certificateFile = Files.createTempFile(getTestName(), ".certificate.der").toFile();
try (FileOutputStream cos = new FileOutputStream(certificateFile))
{
Certificate[] chain = ks.getCertificateChain(TestSSLConstants.CERT_ALIAS_APP1);
for (Certificate pub : chain)
{
cos.write(TestSSLUtils.certificateToPEM(pub).getBytes(UTF_8));
}
cos.flush();
}
return new File[]{privateKeyFile, certificateFile};
}
private File extractCertFileFromTestTrustStore() throws Exception
{
java.security.KeyStore ks = java.security.KeyStore.getInstance(JAVA_KEYSTORE_TYPE);
try (InputStream is = new FileInputStream(TRUSTSTORE))
{
ks.load(is, TRUSTSTORE_PASSWORD.toCharArray());
}
File certificateFile = Files.createTempFile(getTestName(), ".crt").toFile();
try (FileOutputStream cos = new FileOutputStream(certificateFile))
{
for (String alias : Collections.list(ks.aliases()))
{
Certificate pub = ks.getCertificate(alias);
cos.write("-----BEGIN CERTIFICATE-----\n".getBytes());
String base64encoded = Base64.getEncoder().encodeToString(pub.getEncoded());
while (base64encoded.length() > 76)
{
cos.write(base64encoded.substring(0, 76).getBytes());
cos.write("\n".getBytes());
base64encoded = base64encoded.substring(76);
}
cos.write(base64encoded.getBytes());
cos.write("\n-----END CERTIFICATE-----\n".getBytes());
}
cos.flush();
}
return certificateFile;
}
private String getTestPortName()
{
return getTestName() + "TlsPort";
}
private void assertConnection(final Connection connection) throws JMSException
{
assertNotNull("connection should be successful", connection);
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
assertNotNull("create session should be successful", session);
}
private String encodePathOption(final String canonicalPath)
{
try
{
return URLEncoder.encode(canonicalPath, StandardCharsets.UTF_8.name()).replace("+", "%20");
}
catch (UnsupportedEncodingException e)
{
throw new RuntimeException(e);
}
}
}