blob: 1908f0cb6ff901bfadd18280907d4461ac0929f9 [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 java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.internal.verification.VerificationModeFactory.times;
import java.nio.file.Path;
import java.security.PrivateKey;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.KeyManager;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import org.mockito.ArgumentMatcher;
import org.apache.qpid.server.configuration.IllegalConfigurationException;
import org.apache.qpid.server.logging.EventLogger;
import org.apache.qpid.server.logging.LogMessage;
import org.apache.qpid.server.logging.MessageLogger;
import org.apache.qpid.server.logging.messages.KeyStoreMessages;
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.KeyStore;
import org.apache.qpid.test.utils.tls.KeyCertificatePair;
import org.apache.qpid.test.utils.tls.TlsResource;
import org.apache.qpid.test.utils.tls.TlsResourceBuilder;
import org.apache.qpid.server.util.DataUrlUtils;
import org.apache.qpid.test.utils.UnitTestBase;
import org.apache.qpid.test.utils.tls.TlsResourceHelper;
public class NonJavaKeyStoreTest extends UnitTestBase
{
@ClassRule
public static final TlsResource TLS_RESOURCE = new TlsResource();
private static final String DN_FOO = "CN=foo";
private static final String NAME = "myTestTrustStore";
private static final String NON_JAVA_KEY_STORE = "NonJavaKeyStore";
private static final Broker BROKER = BrokerTestHelper.createBrokerMock();
private static final ConfiguredObjectFactory FACTORY = BrokerModel.getInstance().getObjectFactory();
private MessageLogger _messageLogger;
private KeyCertificatePair _keyCertPair;
@Before
public void setUp() throws Exception
{
_messageLogger = mock(MessageLogger.class);
when(BROKER.getEventLogger()).thenReturn(new EventLogger(_messageLogger));
_keyCertPair = generateSelfSignedCertificate();
}
@Test
public void testCreationOfTrustStoreFromValidPrivateKeyAndCertificateInDERFormat() throws Exception
{
final Path privateKeyFile = TLS_RESOURCE.savePrivateKeyAsDer(_keyCertPair.getPrivateKey());
final Path certificateFile = TLS_RESOURCE.saveCertificateAsDer(_keyCertPair.getCertificate());
assertCreationOfTrustStoreFromValidPrivateKeyAndCertificate(privateKeyFile, certificateFile);
}
@Test
public void testCreationOfTrustStoreFromValidPrivateKeyAndCertificateInPEMFormat() throws Exception
{
final Path privateKeyFile = TLS_RESOURCE.savePrivateKeyAsPem(_keyCertPair.getPrivateKey());
final Path certificateFile = TLS_RESOURCE.saveCertificateAsPem(_keyCertPair.getCertificate());
assertCreationOfTrustStoreFromValidPrivateKeyAndCertificate(privateKeyFile, certificateFile);
}
private void assertCreationOfTrustStoreFromValidPrivateKeyAndCertificate(Path privateKeyFile, Path certificateFile) throws Exception
{
Map<String,Object> attributes = new HashMap<>();
attributes.put(NonJavaKeyStore.NAME, NAME);
attributes.put("privateKeyUrl", privateKeyFile.toFile().getAbsolutePath());
attributes.put("certificateUrl", certificateFile.toFile().getAbsolutePath());
attributes.put(NonJavaKeyStore.TYPE, NON_JAVA_KEY_STORE);
final NonJavaKeyStore<?> fileTrustStore = (NonJavaKeyStore<?>) createTestKeyStore(attributes);
KeyManager[] keyManagers = fileTrustStore.getKeyManagers();
assertNotNull(keyManagers);
assertEquals("Unexpected number of key managers", 1, keyManagers.length);
assertNotNull("Key manager is null", keyManagers[0]);
}
@Test
public void testCreationOfTrustStoreFromValidPrivateKeyAndInvalidCertificate()throws Exception
{
final Path privateKeyFile = TLS_RESOURCE.savePrivateKeyAsPem(_keyCertPair.getPrivateKey());
final Path certificateFile = TLS_RESOURCE.createFile(".cer");
Map<String,Object> attributes = new HashMap<>();
attributes.put(NonJavaKeyStore.NAME, NAME);
attributes.put("privateKeyUrl", privateKeyFile.toFile().getAbsolutePath());
attributes.put("certificateUrl", certificateFile.toFile().getAbsolutePath());
attributes.put(NonJavaKeyStore.TYPE, NON_JAVA_KEY_STORE);
KeyStoreTestHelper.checkExceptionThrownDuringKeyStoreCreation(FACTORY, BROKER, KeyStore.class, attributes,
"Cannot load private key or certificate(s): java.security.cert.CertificateException: " +
"Could not parse certificate: java.io.IOException: Empty input");
}
@Test
public void testCreationOfTrustStoreFromInvalidPrivateKeyAndValidCertificate()throws Exception
{
final Path privateKeyFile = TLS_RESOURCE.createFile(".pk");
final Path certificateFile = TLS_RESOURCE.saveCertificateAsPem(_keyCertPair.getCertificate());
Map<String,Object> attributes = new HashMap<>();
attributes.put(NonJavaKeyStore.NAME, NAME);
attributes.put("privateKeyUrl", privateKeyFile.toFile().getAbsolutePath());
attributes.put("certificateUrl", certificateFile.toFile().getAbsolutePath());
attributes.put(NonJavaKeyStore.TYPE, NON_JAVA_KEY_STORE);
KeyStoreTestHelper.checkExceptionThrownDuringKeyStoreCreation(FACTORY, BROKER, KeyStore.class, attributes,
"Cannot load private key or certificate(s): java.security.spec.InvalidKeySpecException: " +
"Unable to parse key as PKCS#1 format");
}
@Test
public void testExpiryCheckingFindsExpired() throws Exception
{
doCertExpiryChecking(1);
verify(_messageLogger, times(1)).message(argThat(new LogMessageArgumentMatcher()));
}
@Test
public void testExpiryCheckingIgnoresValid() throws Exception
{
doCertExpiryChecking(-1);
verify(_messageLogger, never()).message(argThat(new LogMessageArgumentMatcher()));
}
private void doCertExpiryChecking(final int expiryOffset) throws Exception
{
when(BROKER.scheduleHouseKeepingTask(anyLong(), any(TimeUnit.class), any(Runnable.class))).thenReturn(mock(ScheduledFuture.class));
final Path privateKeyFile = TLS_RESOURCE.savePrivateKeyAsDer(_keyCertPair.getPrivateKey());
final Path certificateFile = TLS_RESOURCE.saveCertificateAsDer(_keyCertPair.getCertificate());
final long expiryDays = ChronoUnit.DAYS.between(Instant.now(), _keyCertPair.getCertificate().getNotAfter().toInstant());
Map<String,Object> attributes = new HashMap<>();
attributes.put(NonJavaKeyStore.NAME, NAME);
attributes.put("privateKeyUrl", privateKeyFile.toFile().getAbsolutePath());
attributes.put("certificateUrl", certificateFile.toFile().getAbsolutePath());
attributes.put("context", Collections.singletonMap(KeyStore.CERTIFICATE_EXPIRY_WARN_PERIOD, expiryDays + expiryOffset));
attributes.put(NonJavaKeyStore.TYPE, NON_JAVA_KEY_STORE);
createTestKeyStore(attributes);
}
@Test
public void testCreationOfKeyStoreWithNonMatchingPrivateKeyAndCertificate()throws Exception
{
final KeyCertificatePair keyCertPair2 = generateSelfSignedCertificate();
final Map<String,Object> attributes = new HashMap<>();
attributes.put(NonJavaKeyStore.NAME, NAME);
attributes.put(NonJavaKeyStore.PRIVATE_KEY_URL, getPrivateKeyAsDataUrl(_keyCertPair.getPrivateKey()));
attributes.put(NonJavaKeyStore.CERTIFICATE_URL, getCertificateAsDataUrl(keyCertPair2.getCertificate()));
attributes.put(NonJavaKeyStore.TYPE, NON_JAVA_KEY_STORE);
KeyStoreTestHelper.checkExceptionThrownDuringKeyStoreCreation(FACTORY, BROKER, KeyStore.class, attributes,
"Private key does not match certificate");
}
@Test
public void testUpdateKeyStoreToNonMatchingCertificate()throws Exception
{
final Map<String,Object> attributes = new HashMap<>();
attributes.put(NonJavaKeyStore.NAME, getTestName());
attributes.put(NonJavaKeyStore.PRIVATE_KEY_URL, getPrivateKeyAsDataUrl(_keyCertPair.getPrivateKey()));
attributes.put(NonJavaKeyStore.CERTIFICATE_URL, getCertificateAsDataUrl(_keyCertPair.getCertificate()));
attributes.put(NonJavaKeyStore.TYPE, NON_JAVA_KEY_STORE);
final KeyStore<?> trustStore = createTestKeyStore(attributes);
final KeyCertificatePair keyCertPair2 = generateSelfSignedCertificate();
try
{
final String certUrl = getCertificateAsDataUrl(keyCertPair2.getCertificate());
trustStore.setAttributes(Collections.singletonMap("certificateUrl", certUrl));
fail("Created key store from invalid certificate");
}
catch(IllegalConfigurationException e)
{
// pass
}
}
@SuppressWarnings("unchecked")
private KeyStore<?> createTestKeyStore(final Map<String, Object> attributes)
{
return (KeyStore<?>) FACTORY.create(KeyStore.class, attributes, BROKER);
}
private String getCertificateAsDataUrl(final X509Certificate certificate) throws CertificateEncodingException
{
return DataUrlUtils.getDataUrlForBytes(TlsResourceHelper.toPEM(certificate).getBytes(UTF_8));
}
private String getPrivateKeyAsDataUrl(final PrivateKey privateKey)
{
return DataUrlUtils.getDataUrlForBytes(TlsResourceHelper.toPEM(privateKey).getBytes(UTF_8));
}
private KeyCertificatePair generateSelfSignedCertificate() throws Exception
{
return TlsResourceBuilder.createSelfSigned(DN_FOO);
}
private static class LogMessageArgumentMatcher implements ArgumentMatcher<LogMessage>
{
@Override
public boolean matches(final LogMessage arg)
{
return arg.getLogHierarchy().equals(KeyStoreMessages.EXPIRING_LOG_HIERARCHY);
}
}
}