package org.jivesoftware.openfire.keystore;
import java.math.BigInteger;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Provider;
import java.security.SecureRandom;
import java.security.Security;
import java.security.cert.CertPathBuilder;
import java.security.cert.CertPathBuilderResult;
import java.security.cert.CertStore;
import java.security.cert.CollectionCertStoreParameters;
import java.security.cert.PKIXBuilderParameters;
import java.security.cert.PKIXCertPathBuilderResult;
import java.security.cert.TrustAnchor;
import java.security.cert.X509CertSelector;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.BasicConstraints;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.jivesoftware.util.Base64;
/**
* Utility functions that are intended to be used by unit tests.
*
* @author Guus der Kinderen, guus.der.kinderen@gmail.com
*/
public class KeystoreTestUtils
{
private static final Provider PROVIDER = new BouncyCastleProvider();
private static final Object BEGIN_CERT = "-----BEGIN CERTIFICATE-----";
private static final Object END_CERT = "-----END CERTIFICATE-----";
static
{
// Add the BC provider to the list of security providers
Security.addProvider( PROVIDER );
}
/**
* Returns the Privacy Enhanced Mail (PEM) format of a X509 certificate.
*
* @param certificate An X509 certificate (cannot be null).
* @return a PEM representation of the certificate (never null, never an empty string).
*/
public static String toPemFormat( X509Certificate certificate ) throws Exception {
final StringBuilder sb = new StringBuilder();
sb.append( BEGIN_CERT ).append( '\n' );
sb.append( Base64.encodeBytes( certificate.getEncoded() ) ).append( '\n' );
sb.append( END_CERT).append( '\n' );
return sb.toString();
}
/**
* Generates a chain of certificates, where the first certificate represents the end-entity certificate and the last
* certificate represents the trust anchor (the 'root certificate').
*
* Exactly four certificates are returned:
* <ol>
* <li>The end-entity certificate</li>
* <li>an intermediate CA certificate</li>
* <li>a different intermediate CA certificate</li>
* <li>a root CA certificate</li>
* </ol>
*
* Each certificate is issued by the certificate that's in the next position of the chain. The last certificate is
* self-signed.
*
* @return an array of certificates. Never null, never an empty array.
*/
public static X509Certificate[] generateValidCertificateChain() throws Exception
{
int length = 4;
final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance( "RSA" );
keyPairGenerator.initialize( 512 );
// Root certificate (representing the CA) is self-signed.
KeyPair subjectKeyPair = keyPairGenerator.generateKeyPair();
KeyPair issuerKeyPair = subjectKeyPair;
final X509Certificate[] result = new X509Certificate[ length ];
for ( int i = length - 1 ; i >= 0; i-- )
{
result[ i ] = generateTestCertificate( true, issuerKeyPair, subjectKeyPair, i );
// Further away from the root CA, each certificate is issued by the previous subject.
issuerKeyPair = subjectKeyPair;
subjectKeyPair = keyPairGenerator.generateKeyPair();
}
return result;
}
/**
* Generates a chain of certificates, identical to {@link #generateValidCertificateChain()}, with one exception:
* the second certificate (the first intermediate) is expired.
*
* @return an array of certificates. Never null, never an empty array.
*/
public static X509Certificate[] generateCertificateChainWithExpiredIntermediateCert() throws Exception
{
int length = 4;
final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance( "RSA" );
keyPairGenerator.initialize( 512 );
// Root certificate (representing the CA) is self-signed.
KeyPair subjectKeyPair = keyPairGenerator.generateKeyPair();
KeyPair issuerKeyPair = subjectKeyPair;
final X509Certificate[] result = new X509Certificate[ length ];
for ( int i = length - 1 ; i >= 0; i-- )
{
boolean isValid = ( i != 1 ); // second certificate needs to be expired!
result[ i ] = generateTestCertificate( isValid, issuerKeyPair, subjectKeyPair, i );
// Further away from the root CA, each certificate is issued by the previous subject.
issuerKeyPair = subjectKeyPair;
subjectKeyPair = keyPairGenerator.generateKeyPair();
}
return result;
}
/**
* Generates a chain of certificates, identical to {@link #generateValidCertificateChain()}, with one exception:
* the last certificate (the root CA) is expired.
*
* @return an array of certificates. Never null, never an empty array.
*/
public static X509Certificate[] generateCertificateChainWithExpiredRootCert() throws Exception
{
int length = 4;
final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance( "RSA" );
keyPairGenerator.initialize( 512 );
// Root certificate (representing the CA) is self-signed.
KeyPair subjectKeyPair = keyPairGenerator.generateKeyPair();
KeyPair issuerKeyPair = subjectKeyPair;
final X509Certificate[] result = new X509Certificate[ length ];
for ( int i = length - 1 ; i >= 0; i-- )
{
boolean isValid = ( i != length - 1 ); // root certificate needs to be expired!
result[ i ] = generateTestCertificate( isValid, issuerKeyPair, subjectKeyPair, i );
// Further away from the root CA, each certificate is issued by the previous subject.
issuerKeyPair = subjectKeyPair;
subjectKeyPair = keyPairGenerator.generateKeyPair();
}
return result;
}
private static X509Certificate generateTestCertificate( final boolean isValid, final KeyPair issuerKeyPair, final KeyPair subjectKeyPair, int indexAwayFromEndEntity) throws Exception
{
// Issuer and Subject.
final X500Name subject = new X500Name( "CN=" + Base64.encodeBytes( subjectKeyPair.getPublic().getEncoded(), Base64.URL_SAFE ) );
final X500Name issuer = new X500Name( "CN=" + Base64.encodeBytes( issuerKeyPair.getPublic().getEncoded(), Base64.URL_SAFE ) );
// Validity
final Date notBefore;
final Date notAfter;
if ( isValid )
{
notBefore = new Date( System.currentTimeMillis() - ( 1000L * 60 * 60 * 24 * 30 ) ); // 30 days ago
notAfter = new Date( System.currentTimeMillis() + ( 1000L * 60 * 60 * 24 * 99 ) ); // 99 days from now.
}
else
{
// Generate a certificate for which the validate period has expired.
notBefore = new Date( System.currentTimeMillis() - ( 1000L * 60 * 60 * 24 * 40 ) ); // 40 days ago
notAfter = new Date( System.currentTimeMillis() - ( 1000L * 60 * 60 * 24 * 10 ) ); // 10 days ago
}
// The new certificate should get a unique serial number.
final BigInteger serial = BigInteger.valueOf( Math.abs( new SecureRandom().nextInt() ) );
final X509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder(
issuer,
serial,
notBefore,
notAfter,
subject,
subjectKeyPair.getPublic()
);
// When this certificate is used to sign another certificate, basic constraints need to be set.
if ( indexAwayFromEndEntity > 0 )
{
builder.addExtension( Extension.basicConstraints, true, new BasicConstraints( indexAwayFromEndEntity - 1 ) );
}
final ContentSigner contentSigner = new JcaContentSignerBuilder( "SHA1withRSA" ).build( issuerKeyPair.getPrivate() );
final X509CertificateHolder certificateHolder = builder.build( contentSigner );
return new JcaX509CertificateConverter().setProvider( "BC" ).getCertificate( certificateHolder );
}
/**
* This method will validate a chain of certificates. It is provided as an alternative to the certificate chain
* validation mechanisms that are under test. This method is intended to be used as a comparative benchmark against
* other validation methods.
*
* The first certificate in the chain is expected to be the end-entity certificate.
*
* The last certificate in the chain is expected to be the root CA certificate.
*
* @param chain A certificate chain (cannot be null or empty).
* @return CertPathBuilderResult result of validation.
* @throws Exception When the chain is not valid.
*/
public CertPathBuilderResult testChain( X509Certificate[] chain ) throws Exception
{
// Create the selector that specifies the starting certificate
X509CertSelector selector = new X509CertSelector();
selector.setCertificate( chain[0] );
// Create the trust anchors (set of root CA certificates)
Set<TrustAnchor> trustAnchors = new HashSet<TrustAnchor>();
trustAnchors.add(new TrustAnchor(chain[ chain.length - 1], null));
// Configure the PKIX certificate builder algorithm parameters
PKIXBuilderParameters pkixParams = new PKIXBuilderParameters(
trustAnchors, selector);
// Disable CRL checks (this is done manually as additional step)
pkixParams.setRevocationEnabled(false);
// Specify a list of intermediate certificates
Set<java.security.cert.Certificate> intermediateCerts = new HashSet<>();
for (int i=1; i<chain.length -1; i++)
{
intermediateCerts.add( chain[ i ] );
}
CertStore intermediateCertStore = CertStore.getInstance("Collection",
new CollectionCertStoreParameters(intermediateCerts));
pkixParams.addCertStore(intermediateCertStore);
// Build and verify the certification chain
CertPathBuilder builder = CertPathBuilder.getInstance("PKIX");
PKIXCertPathBuilderResult result = (PKIXCertPathBuilderResult) builder
.build(pkixParams);
return result;
}
/**
* Instantiates a new certificate of which the notAfter value is a point in time that is in the past (as compared
* to the point in time of the invocation of this method).
*
* @return A certificate that is invalid (never null).
*/
public static X509Certificate generateExpiredCertificate() throws Exception
{
return generateTestCertificate( false, false, 0 );
}
/**
* Instantiates a new certificate of which the notBefore value is a point in the past, and the notAfter value is a
* point in the future (as compared to the point in time of the invocation of this method).
*
* The notAfter value can be expected to be a value that is far enough in the future for unit testing purposes, but
* should not be assumed to be a value that is in the distant future. It is safe to assume that the generated
* certificate will remain to be valid for the duration of a generic unit test (which is measured in seconds or
* fractions thereof).
*
* @return A certificate that is valid (never null).
*/
public static X509Certificate generateValidCertificate() throws Exception
{
return generateTestCertificate( true, false, 0 );
}
/**
* Instantiates a new certificate that is self-signed, meaning that the issuer and subject values are identical. The
* returned certificate is valid in the same manner as described in the documentation of
* {@link #generateSelfSignedCertificate()}.
*
* @return A certificate that is self-signed (never null).
* @see #generateValidCertificate()
*/
public static X509Certificate generateSelfSignedCertificate() throws Exception
{
return generateTestCertificate( true, true, 0 );
}
/**
* Instantiates a new certificate that is self-signed, of which the notAfter value is a point in time that is in the
* past (as compared to the point in time of the invocation of this method).
*
* @return A certificate that is self-signed and expired (never null).
* @see #generateSelfSignedCertificate()
* @see #generateExpiredCertificate()
*/
public static X509Certificate generateExpiredSelfSignedCertificate() throws Exception
{
return generateTestCertificate( false, true, 0 );
}
private static X509Certificate generateTestCertificate( final boolean isValid, final boolean isSelfSigned, int indexAwayFromEndEntity ) throws Exception
{
final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance( "RSA" );
keyPairGenerator.initialize( 512 );
final KeyPair subjectKeyPair;
final KeyPair issuerKeyPair;
if ( isSelfSigned )
{
// Self signed: subject and issuer are the same entity.
subjectKeyPair = keyPairGenerator.generateKeyPair();
issuerKeyPair = subjectKeyPair;
}
else
{
subjectKeyPair = keyPairGenerator.generateKeyPair();
issuerKeyPair = keyPairGenerator.generateKeyPair();
}
return generateTestCertificate( isValid, issuerKeyPair, subjectKeyPair, indexAwayFromEndEntity );
}
}