blob: 7c968cfde0ae6a9355acee908600dd667589146b [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.server.security;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.file.Path;
import java.security.SecureRandom;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLServerSocketFactory;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import org.apache.qpid.server.configuration.IllegalConfigurationException;
import org.apache.qpid.server.model.Broker;
import org.apache.qpid.server.model.BrokerModel;
import org.apache.qpid.server.model.BrokerTestHelper;
import org.apache.qpid.server.model.ConfiguredObjectFactory;
import org.apache.qpid.server.model.TrustStore;
import org.apache.qpid.server.transport.network.security.ssl.SSLUtil;
import org.apache.qpid.test.utils.UnitTestBase;
import org.apache.qpid.test.utils.tls.KeyCertificatePair;
import org.apache.qpid.test.utils.tls.PrivateKeyEntry;
import org.apache.qpid.test.utils.tls.TlsResource;
import org.apache.qpid.test.utils.tls.TlsResourceBuilder;
import org.apache.qpid.test.utils.tls.TlsResourceHelper;
public class SiteSpecificTrustStoreTest extends UnitTestBase
{
@ClassRule
public static final TlsResource TLS_RESOURCE = new TlsResource();
private static final Broker BROKER = BrokerTestHelper.createBrokerMock();
private static final ConfiguredObjectFactory FACTORY = BrokerModel.getInstance().getObjectFactory();
private static final String EXPECTED_SUBJECT = "CN=localhost";
private static final String EXPECTED_ISSUER = "CN=MyRootCA";
private static final String DN_BAR = "CN=bar";
private static final String NAME = "mySiteSpecificTrustStore";
private static final String SITE_SPECIFIC_TRUST_STORE = "SiteSpecificTrustStore";
private static final String NOT_SUPPORTED_URL = "file:/not/a/host";
private static final String INVALID_URL = "notaurl:541";
private static final String NOT_A_CRL = "/not/a/crl";
private TestPeer _testPeer;
private String _clrUrl;
private KeyCertificatePair _caKeyCertPair;
private KeyCertificatePair _keyCertPair;
@Before
public void setUpSiteSpecificTrustStore() throws Exception
{
int connectTimeout = Integer.getInteger("SiteSpecificTrustStoreTest.connectTimeout", 1000);
int readTimeout = Integer.getInteger("SiteSpecificTrustStoreTest.readTimeout", 1000);
setTestSystemProperty(SiteSpecificTrustStore.TRUST_STORE_SITE_SPECIFIC_CONNECT_TIMEOUT,
String.valueOf(connectTimeout));
setTestSystemProperty(SiteSpecificTrustStore.TRUST_STORE_SITE_SPECIFIC_READ_TIMEOUT,
String.valueOf(readTimeout));
_caKeyCertPair = TlsResourceBuilder.createKeyPairAndRootCA(EXPECTED_ISSUER);
_keyCertPair = TlsResourceBuilder.createKeyPairAndCertificate(EXPECTED_SUBJECT, _caKeyCertPair);
final KeyCertificatePair keyCertPair2 = TlsResourceBuilder.createKeyPairAndCertificate(DN_BAR, _caKeyCertPair);
_clrUrl = TLS_RESOURCE.createCrlAsDataUrl(_caKeyCertPair, keyCertPair2.getCertificate());
}
@After
public void tearDown() throws Exception
{
if (_testPeer != null)
{
_testPeer.close();
}
}
@Test
public void testMalformedSiteUrl()
{
Map<String, Object> attributes = new HashMap<>();
attributes.put(SiteSpecificTrustStore.NAME, NAME);
attributes.put(SiteSpecificTrustStore.TYPE, SITE_SPECIFIC_TRUST_STORE);
attributes.put("siteUrl", INVALID_URL);
KeyStoreTestHelper.checkExceptionThrownDuringKeyStoreCreation(FACTORY,
BROKER,
TrustStore.class,
attributes,
String.format("'%s' is not a valid URL",
INVALID_URL));
}
@Test
public void testSiteUrlDoesNotSupplyHostPort()
{
Map<String, Object> attributes = new HashMap<>();
attributes.put(SiteSpecificTrustStore.NAME, NAME);
attributes.put(SiteSpecificTrustStore.TYPE, SITE_SPECIFIC_TRUST_STORE);
attributes.put("siteUrl", NOT_SUPPORTED_URL);
KeyStoreTestHelper.checkExceptionThrownDuringKeyStoreCreation(FACTORY, BROKER,
TrustStore.class,
attributes,
String.format(
"URL '%s' does not provide a hostname and port number",
NOT_SUPPORTED_URL));
}
@Test
public void testUnresponsiveSite() throws Exception
{
_testPeer = new TestPeer();
_testPeer.setAccept(false);
int listeningPort = _testPeer.start();
Map<String, Object> attributes = getTrustStoreAttributes(listeningPort);
KeyStoreTestHelper.checkExceptionThrownDuringKeyStoreCreation(FACTORY, BROKER,
TrustStore.class,
attributes,
String.format(
"Unable to get certificate for '%s' from", NAME));
}
@Test
public void testValidSiteUrl() throws Exception
{
_testPeer = new TestPeer();
int listeningPort = _testPeer.start();
Map<String, Object> attributes = getTrustStoreAttributes(listeningPort);
attributes.put(SiteSpecificTrustStore.CERTIFICATE_REVOCATION_CHECK_ENABLED, true);
attributes.put(SiteSpecificTrustStore.CERTIFICATE_REVOCATION_LIST_URL, _clrUrl);
final SiteSpecificTrustStore<?> trustStore = createTestTrustStore(attributes);
List<CertificateDetails> certDetails = trustStore.getCertificateDetails();
assertEquals("Unexpected number of certificates", 1, certDetails.size());
CertificateDetails certificateDetails = certDetails.get(0);
assertEquals("Unexpected certificate subject", EXPECTED_SUBJECT, certificateDetails.getSubjectName());
assertEquals("Unexpected certificate issuer", EXPECTED_ISSUER, certificateDetails.getIssuerName());
}
@Test
public void testChangeOfCrlInValidSiteUrl() throws Exception
{
_testPeer = new TestPeer();
int listeningPort = _testPeer.start();
Map<String, Object> attributes = getTrustStoreAttributes(listeningPort);
attributes.put(SiteSpecificTrustStore.CERTIFICATE_REVOCATION_CHECK_ENABLED, true);
attributes.put(SiteSpecificTrustStore.CERTIFICATE_REVOCATION_LIST_URL, _clrUrl);
final SiteSpecificTrustStore<?> trustStore = createTestTrustStore(attributes);
try
{
trustStore.setAttributes(Collections.singletonMap(FileTrustStore.CERTIFICATE_REVOCATION_LIST_URL,
NOT_A_CRL));
fail("Exception not thrown");
}
catch (IllegalConfigurationException e)
{
String message = e.getMessage();
assertTrue("Exception text not as unexpected:" + message,
message.contains(
String.format("Unable to load certificate revocation list '%s' for truststore '%s'", NOT_A_CRL, NAME)));
}
assertEquals("Unexpected CRL path value after failed change",
_clrUrl, trustStore.getCertificateRevocationListUrl());
final Path emptyCrl = TLS_RESOURCE.createCrl(_caKeyCertPair);
trustStore.setAttributes(Collections.singletonMap(FileTrustStore.CERTIFICATE_REVOCATION_LIST_URL,
emptyCrl.toFile().getAbsolutePath()));
assertEquals("Unexpected CRL path value after change that is expected to be successful",
emptyCrl.toFile().getAbsolutePath(), trustStore.getCertificateRevocationListUrl());
}
@Test
public void testValidSiteUrl_MissingCrlFile() throws Exception
{
_testPeer = new TestPeer();
int listeningPort = _testPeer.start();
Map<String, Object> attributes = getTrustStoreAttributes(listeningPort);
attributes.put(SiteSpecificTrustStore.CERTIFICATE_REVOCATION_CHECK_ENABLED, true);
attributes.put(SiteSpecificTrustStore.CERTIFICATE_REVOCATION_LIST_URL, NOT_A_CRL);
KeyStoreTestHelper.checkExceptionThrownDuringKeyStoreCreation(FACTORY, BROKER,
TrustStore.class,
attributes,
String.format(
"Unable to load certificate revocation list '%s' for truststore '%s'", NOT_A_CRL, NAME));
}
@Test
public void testRefreshCertificate() throws Exception
{
_testPeer = new TestPeer();
int listeningPort = _testPeer.start();
Map<String, Object> attributes = getTrustStoreAttributes(listeningPort);
final SiteSpecificTrustStore<?> trustStore = createTestTrustStore(attributes);
List<CertificateDetails> certDetails = trustStore.getCertificateDetails();
assertEquals("Unexpected number of certificates", 1, certDetails.size());
CertificateDetails certificateDetails = certDetails.get(0);
assertEquals("Unexpected certificate subject", EXPECTED_SUBJECT, certificateDetails.getSubjectName());
assertEquals("Unexpected certificate issuer", EXPECTED_ISSUER, certificateDetails.getIssuerName());
trustStore.refreshCertificate();
certDetails = trustStore.getCertificateDetails();
certificateDetails = certDetails.get(0);
assertEquals("Unexpected certificate subject", EXPECTED_SUBJECT, certificateDetails.getSubjectName());
assertEquals("Unexpected certificate issuer", EXPECTED_ISSUER, certificateDetails.getIssuerName());
}
@SuppressWarnings("unchecked")
private SiteSpecificTrustStore createTestTrustStore(final Map<String, Object> attributes)
{
return (SiteSpecificTrustStore) FACTORY.create(TrustStore.class, attributes, BROKER);
}
private Map<String, Object> getTrustStoreAttributes(final int listeningPort)
{
Map<String, Object> attributes = new HashMap<>();
attributes.put(SiteSpecificTrustStore.NAME, NAME);
attributes.put(SiteSpecificTrustStore.TYPE, SITE_SPECIFIC_TRUST_STORE);
attributes.put("siteUrl", String.format("https://localhost:%d", listeningPort));
return attributes;
}
private class TestPeer implements Closeable
{
private final ExecutorService _socketAcceptExecutor = Executors.newSingleThreadExecutor();
private ServerSocket _serverSocket;
private final AtomicBoolean _shutdown = new AtomicBoolean();
private boolean _accept = true;
public void setAccept(final boolean accept)
{
_accept = accept;
}
public int start() throws Exception
{
_serverSocket = createTestSSLServerSocket();
if (_accept)
{
_socketAcceptExecutor.execute(new AcceptingRunnable());
}
return _serverSocket.getLocalPort();
}
@Override
public void close() throws IOException
{
_shutdown.set(true);
try
{
if (_serverSocket != null)
{
_serverSocket.close();
}
}
finally
{
_socketAcceptExecutor.shutdown();
}
}
private ServerSocket createTestSSLServerSocket() throws Exception
{
char[] secret = "".toCharArray();
java.security.KeyStore inMemoryKeyStore =
TlsResourceHelper.createKeyStore(java.security.KeyStore.getDefaultType(),
secret,
new PrivateKeyEntry("1",
_keyCertPair.getPrivateKey(),
_keyCertPair.getCertificate()));
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(inMemoryKeyStore, secret);
KeyManager[] keyManagers = kmf.getKeyManagers();
SSLContext sslContext = SSLUtil.tryGetSSLContext();
sslContext.init(keyManagers, null, new SecureRandom());
SSLServerSocketFactory socketFactory = sslContext.getServerSocketFactory();
ServerSocket serverSocket = socketFactory.createServerSocket(0);
serverSocket.setSoTimeout(100);
return serverSocket;
}
private class AcceptingRunnable implements Runnable
{
@Override
public void run()
{
do
{
try (Socket sock = _serverSocket.accept())
{
final InputStream inputStream = sock.getInputStream();
while (inputStream.read() != -1)
{
// ignore
}
}
catch (IOException e)
{
// Ignore
}
}
while (!_shutdown.get());
}
}
}
}