package org.jivesoftware.openfire.keystore;
import org.junit.*;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import java.security.KeyStore;
import java.security.cert.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* Unit tests that verify the functionality of {@link OpenfireX509TrustManager#checkChainTrusted(CertSelector, X509Certificate...)}.
*
* @author Guus der Kinderen, guus.der.kinderen@gmail.com
*/
@RunWith( Parameterized.class )
public class CheckChainTrustedTest
{
/**
* All tests in this class are executed repeatedly. A new instance of this class is created for each element in the
* iterable returned here (its values are passed to the constructor of this class). This allows us to execute the
* same set of tests against a different configuration of the system under test.
*/
@Parameterized.Parameters(name = "acceptSelfSignedCertificates={0},checkValidity={1}" )
public static Iterable<Object[]> constructorArguments()
{
final List<Object[]> constructorArguments = new ArrayList<>();
constructorArguments.add( new Object[] { false, true } ); // acceptSelfSignedCertificates = false, check validity = true
constructorArguments.add( new Object[] { false, false } ); // acceptSelfSignedCertificates = false, check validity = false
constructorArguments.add( new Object[] { true, true } ); // acceptSelfSignedCertificates = true, check validity = true
constructorArguments.add( new Object[] { true, false } ); // acceptSelfSignedCertificates = true, check validity = false
return constructorArguments;
}
/**
* Configuration for the system under test: does or does not accept self-signed certificates.
*/
private final boolean acceptSelfSigned;
/**
* Configuration for the system under test: does or does not check current validity (notBefore/notAfter).
*/
private final boolean checkValidity;
/**
* The keystore that contains the certificates used by the system under test (refreshed before every test invocation).
*/
private KeyStore trustStore;
/**
* A valid chain of certificates, where the first certificate represents the end-entity certificate and the last
* certificate represents the trust anchor (the 'root certificate'). This root certificate is present
* in {@link #trustStore} (refreshed before every test invocation).
*/
private X509Certificate[] validChain;
/**
* A chain of certificates where the first certificate represents the end-entity certificate and the last
* certificate represents the trust anchor (the 'root certificate'). This root certificate is present
* in {@link #trustStore}. One of the intermediate certificates in this chain is expired. The chain is otherwise
* valid. (refreshed before every test invocation).
*/
private X509Certificate[] expiredIntChain;
/**
* A chain of certificates where the first certificate represents the end-entity certificate and the last
* certificate represents the trust anchor (the 'root certificate'). This root certificate is present
* in {@link #trustStore}. The root certificate in this chain is expired. The chain is otherwise
* valid. (refreshed before every test invocation).
*/
private X509Certificate[] expiredRootChain;
/**
* The system under test (refreshed before every test invocation).
*/
private OpenfireX509TrustManager trustManager;
public CheckChainTrustedTest( boolean acceptSelfSigned, boolean checkValidity )
{
this.acceptSelfSigned = acceptSelfSigned;
this.checkValidity = checkValidity;
}
@Before
public void createFixture() throws Exception
{
trustStore = KeyStore.getInstance( KeyStore.getDefaultType() );
trustStore.load( null, null );
// Generate a valid chain and add its root certificate to the trust store.
validChain = KeystoreTestUtils.generateValidCertificateChain();
trustStore.setCertificateEntry( getLast( validChain ).getSubjectDN().getName(), getLast( validChain ) );
// Generate a chain with an expired intermediate certificate and add its root certificate to the trust store.
expiredIntChain = KeystoreTestUtils.generateCertificateChainWithExpiredIntermediateCert();
trustStore.setCertificateEntry( getLast( expiredIntChain ).getSubjectDN().getName(), getLast( expiredIntChain ) );
// Generate a chain with an expired root certificate and add its root certificate to the trust store.
expiredRootChain = KeystoreTestUtils.generateCertificateChainWithExpiredRootCert();
trustStore.setCertificateEntry( getLast( expiredRootChain ).getSubjectDN().getName(), getLast( expiredRootChain ) );
// Reset the system under test before each test.
trustManager = new OpenfireX509TrustManager( trustStore, acceptSelfSigned, checkValidity );
}
/**
* Returns the last element from the provided array.
*
* @param chain An array (cannot be null).
* @return The last element of the provided array.
*/
private static <X> X getLast( X[] chain ) {
return chain[ chain.length - 1 ];
}
@After
public void tearDown() throws Exception
{
if ( trustStore != null)
{
trustStore = null;
}
}
/**
* Verifies that providing a null value for the first argument causes a runtime exception to be thrown.
*/
@Test( expected = RuntimeException.class )
public void testNullSelectorArgument() throws Exception
{
// Setup fixture.
final CertSelector selector = null;
final X509Certificate[] chain = validChain;
// Execute system under test.
trustManager.checkChainTrusted( selector, chain );
// Verify results
// (verified by 'expected' parameter of @Test annotation).
}
/**
* Verifies that providing a null value for the first argument causes a runtime exception to be thrown.
*/
@Test( expected = RuntimeException.class )
public void testNullChainArgument() throws Exception
{
// Setup fixture.
final CertSelector selector = new X509CertSelector();
final X509Certificate[] chain = null;
// Execute system under test.
trustManager.checkChainTrusted( selector, chain );
// Verify results
// (verified by 'expected' parameter of @Test annotation).
}
/**
* Verifies that providing an empty array for the first argument causes a runtime exception to be thrown.
*/
@Test( expected = RuntimeException.class )
public void testEmptyChainArgument() throws Exception
{
// Setup fixture.
final CertSelector selector = new X509CertSelector();
final X509Certificate[] chain = new X509Certificate[0];
// Execute system under test.
trustManager.checkChainTrusted( selector, chain );
// Verify results
// (verified by 'expected' parameter of @Test annotation).
}
/**
* Verifies that null values in the provided chain are silently ignored.
*/
@Test
public void testIgnoreEmptyArraySlots() throws Exception
{
// Setup fixture.
final CertSelector selector = new X509CertSelector();
// Inject a null value, moving but not removing all other certificates.
final List<X509Certificate> copy = new ArrayList<>( Arrays.asList( validChain ) ); // wrapping needed to support remove function.
copy.add( 1, null );
final X509Certificate[] chain = copy.toArray( new X509Certificate[ copy.size() ] );
// Execute system under test.
final CertPath result = trustManager.checkChainTrusted( selector, chain );
// Verify results
Assert.assertNotNull( result );
}
/**
* Verifies that providing a complete and valid chain does not throw an exception (selection criteria during this
* test: match on subject). This is a 'happy flow' test.
*/
@Test
public void testFullChain() throws Exception
{
// Setup fixture.
final X509CertSelector selector = new X509CertSelector();
selector.setSubject( validChain[0].getSubjectX500Principal() );
final X509Certificate[] chain = validChain;
// Execute system under test.
final CertPath result = trustManager.checkChainTrusted( selector, chain );
// Verify results
Assert.assertNotNull( result );
}
/**
* Verifies that providing a complete, valid but unordered chain does not throw an exception (selection criteria
* during this test: match on subject).
*/
@Test
public void testFullChainUnordered() throws Exception
{
// Setup fixture.
final X509CertSelector selector = new X509CertSelector();
selector.setSubject( validChain[0].getSubjectX500Principal() );
final List<X509Certificate> input = new ArrayList<>( Arrays.asList( validChain ));
final List<X509Certificate> shuffled = new ArrayList<>( input );
while ( input.equals( shuffled ) ) {
Collections.shuffle( shuffled );
}
final X509Certificate[] chain = shuffled.toArray( new X509Certificate[ shuffled.size() ] );
// Execute system under test.
final CertPath result = trustManager.checkChainTrusted( selector, chain );
// Verify results
Assert.assertNotNull( result );
}
/**
* Verifies that providing a valid chain that does not contain the root certificate does not throw an exception
* (selection criteria during this test: match on subject). This is a 'happy flow' test.
*/
@Test
public void testPartialChain() throws Exception
{
// Setup fixture.
final X509CertSelector selector = new X509CertSelector();
selector.setSubject( validChain[0].getSubjectX500Principal() );
final X509Certificate[] chain = Arrays.copyOf( validChain, validChain.length - 1);
// Execute system under test.
final CertPath result = trustManager.checkChainTrusted( selector, chain );
// Verify results
Assert.assertNotNull( result );
}
/**
* Verifies that providing a valid chain that does not contain the root certificate and is not ordered does not
* throw an exception (selection criteria during this test: match on subject).
*/
@Test
public void testPartialChainUnordered() throws Exception
{
// Setup fixture.
final X509CertSelector selector = new X509CertSelector();
selector.setSubject( validChain[0].getSubjectX500Principal() );
final List<X509Certificate> input = new ArrayList<>( Arrays.asList( validChain ));
input.remove( input.size() - 1 );
final List<X509Certificate> shuffled = new ArrayList<>( input );
while ( input.equals( shuffled ) ) {
Collections.shuffle( shuffled );
}
final X509Certificate[] chain = shuffled.toArray( new X509Certificate[ shuffled.size() ] );
// Execute system under test.
final CertPath result = trustManager.checkChainTrusted( selector, chain );
// Verify results
Assert.assertNotNull( result );
}
/**
* Verifies that providing a chain that does not contain an intermediate certificate throws an exception
* (selection criteria during this test: match on subject).
*/
@Test( expected = CertPathBuilderException.class )
public void testIncompleteChain() throws Exception
{
// Setup fixture.
final X509CertSelector selector = new X509CertSelector();
selector.setSubject( validChain[0].getSubjectX500Principal() );
// Copy all but the second certificate of a valid chain to a chain that's used for testing.
final List<X509Certificate> copy = new ArrayList<>( Arrays.asList( validChain ) ); // wrapping needed to support remove function.
copy.remove( 1 );
final X509Certificate[] chain = copy.toArray( new X509Certificate[ copy.size() ] );
// Execute system under test.
trustManager.checkChainTrusted( selector, chain );
// Verify results
// (verified by 'expected' parameter of @Test annotation).
}
/**
* Verifies that providing chain for which the root certificate is not in the store (but is otherwise valid)
* throw a CertPathBuilderException.
*/
@Test( expected = CertPathBuilderException.class )
public void testFullChainUnrecognizedRoot() throws Exception
{
// Setup fixture.
final X509CertSelector selector = new X509CertSelector();
selector.setSubject( validChain[0].getSubjectX500Principal() );
final X509Certificate[] chain = KeystoreTestUtils.generateValidCertificateChain();
// Execute system under test.
trustManager.checkChainTrusted( selector, chain );
// Verify results
// (verified by 'expected' parameter of @Test annotation).
}
/**
* Verifies that providing chain for which an intermediate certificate is expired (but is otherwise valid)
* throws a CertPathBuilderException only when the system under test is configured to enforce date validity.
*/
@Test
public void testExpiredIntermediateChain() throws Exception
{
// Setup fixture.
final X509CertSelector selector = new X509CertSelector();
selector.setSubject( expiredIntChain[0].getSubjectX500Principal() );
final X509Certificate[] chain = expiredIntChain;
// Execute system under test.
CertPath result = null;
CertPathBuilderException exception = null;
try
{
result = trustManager.checkChainTrusted( selector, chain );
}
catch ( CertPathBuilderException ex)
{
exception = ex;
}
// Verify results
if ( checkValidity )
{
Assert.assertNotNull( "Certificate validity is enforced. Validation should have thrown an exception.", exception );
}
else
{
Assert.assertNotNull( "Certificate validity is not checked. Validation should have succeeded.", result );
}
}
/**
* Verifies that providing chain for which the root certificate is expired (but is otherwise valid)
* throws a CertPathBuilderException only when the system under test is configured to enforce date validity.
*/
@Test
public void testExpiredRootChain() throws Exception
{
// Setup fixture.
final X509CertSelector selector = new X509CertSelector();
selector.setSubject( expiredRootChain[0].getSubjectX500Principal() );
final X509Certificate[] chain = expiredRootChain;
// Execute system under test.
CertPath result = null;
CertPathBuilderException exception = null;
try
{
result = trustManager.checkChainTrusted( selector, chain );
}
catch ( CertPathBuilderException ex)
{
exception = ex;
}
// Verify results
if ( checkValidity )
{
Assert.assertNotNull( "Certificate validity is enforced. Validation should have thrown an exception.", exception );
}
else
{
Assert.assertNotNull( "Certificate validity is not checked. Validation should have succeeded.", result );
}
}
/**
* Identical to {@link #testExpiredRootChain()} but uses a partial chain (which does not include the root
* certificate).
*/
@Test
public void testExpiredRootChainPartial() throws Exception
{
// Setup fixture.
final X509CertSelector selector = new X509CertSelector();
selector.setSubject( expiredRootChain[0].getSubjectX500Principal() );
final X509Certificate[] chain = Arrays.copyOf( expiredRootChain, expiredRootChain.length - 1);
// Execute system under test.
CertPath result = null;
CertPathBuilderException exception = null;
try
{
result = trustManager.checkChainTrusted( selector, chain );
}
catch ( CertPathBuilderException ex)
{
exception = ex;
}
// Verify results
if ( checkValidity )
{
Assert.assertNotNull( "Certificate validity is enforced. Validation should have thrown an exception.", exception );
}
else
{
Assert.assertNotNull( "Certificate validity is not checked. Validation should have succeeded.", result );
}
}
/**
* Verifies that self-signed certificates are accepted, but only when explicitly configured to do so.
*/
@Test
public void testSelfSigned() throws Exception
{
// Setup fixture.
final X509Certificate[] chain = new X509Certificate[] { KeystoreTestUtils.generateSelfSignedCertificate() };
final X509CertSelector selector = new X509CertSelector();
selector.setSubject( chain[ 0 ].getSubjectX500Principal() );
// Execute system under test.
CertPath result = null;
CertPathBuilderException exception = null;
try
{
result = trustManager.checkChainTrusted( selector, chain );
}
catch ( CertPathBuilderException ex)
{
exception = ex;
}
// Verify results
if ( acceptSelfSigned )
{
Assert.assertNotNull( "Self-signed certificates are accepted. Validation should have succeeded.", result );
}
else
{
Assert.assertNotNull( "Self-signed certificates are not accepted. Validation should have thrown an exception.", exception );
}
}
/**
* Verifies that self-signed certificates that expired are accepted only when both self-signed certificates are
* explicitly accepted, as well as validation is explictly skipped.
*/
@Test
public void testSelfSignedExpired() throws Exception
{
// Setup fixture.
final X509Certificate[] chain = new X509Certificate[] { KeystoreTestUtils.generateExpiredSelfSignedCertificate() };
final X509CertSelector selector = new X509CertSelector();
selector.setSubject( chain[ 0 ].getSubjectX500Principal() );
// Execute system under test.
CertPath result = null;
CertPathBuilderException exception = null;
try
{
result = trustManager.checkChainTrusted( selector, chain );
}
catch ( CertPathBuilderException ex)
{
exception = ex;
}
// Verify results
if ( acceptSelfSigned && !checkValidity)
{
Assert.assertNotNull( "Certificate validity is not checked, and self-signed certificates are accepted. Validation should have succeeded.", result );
}
else
{
final StringBuilder sb = new StringBuilder();
if ( checkValidity )
{
sb.append( "Certificate validity is checked. " );
}
if ( !acceptSelfSigned )
{
sb.append( "Self-signed certificates are not accepted. " );
}
Assert.assertNotNull( sb.toString() + "Validation should have thrown an exception.", exception );
}
}
}