Commit 97f7cf3f authored by Guus der Kinderen's avatar Guus der Kinderen

OF-946: Openfire should allow for more than one set of keystores.

This commit:
- Refactors SSL-related code to reduce complexity and deprecate unused code
- Adds factory methods to obtain SSL/TLS engine / contexts, to replace all
  code duplication that was going on before.
parent bd2c086b
......@@ -354,7 +354,7 @@ public interface Connection extends Closeable {
* otherwise a {@link org.jivesoftware.openfire.net.ServerTrustManager} will be used.
* @param authentication policy to use for authenticating the remote peer.
* @throws Exception if an error occured while securing the connection.
* @deprecated Use {@link #startTLS(boolean, boolean, ClientAuth)} instead, with isClientToServer = (remoteServer == null)
* @deprecated Use {@link #startTLS(boolean, boolean, ClientAuth)} instead, with isPeerClient = (remoteServer == null)
*/
@Deprecated
void startTLS(boolean clientMode, String remoteServer, ClientAuth authentication) throws Exception;
......@@ -373,13 +373,13 @@ public interface Connection extends Closeable {
* <tt>remoteServer</tt> parameter will always be <tt>null</tt>.
*
* @param clientMode boolean indicating if this entity is a client or a server in the TLS negotiation.
* @param isClientToServer indicates if the remote party is a server. When false a
* @param isPeerClient indicates if the remote party is a client. When true a
* {@link org.jivesoftware.openfire.net.ClientTrustManager} will be used for verifying certificates
* otherwise a {@link org.jivesoftware.openfire.net.ServerTrustManager} will be used.
* @param authentication policy to use for authenticating the remote peer.
* @throws Exception if an error occured while securing the connection.
*/
void startTLS(boolean clientMode, boolean isClientToServer, ClientAuth authentication) throws Exception;
void startTLS(boolean clientMode, boolean isPeerClient, ClientAuth authentication) throws Exception;
/**
* Adds the compression filter to the connection but only filter incoming traffic. Do not filter
......
......@@ -63,7 +63,7 @@ public class SSLProtocolSocketFactory implements SecureProtocolSocketFactory {
private SSLContext createSSLContext(String host) {
try {
final SSLContext context = SSLConfig.getSSLContext();
final SSLContext context = SSLConfig.getSSLContext( SSLConfig.Type.ADMIN );
context.init(
null,
new TrustManager[] {
......
package org.jivesoftware.openfire.keystore;
import java.security.Principal;
import java.security.cert.*;
import java.util.*;
/**
* Utility methods for working with {@link javax.security.cert.Certificate} instances.
*
* @author Guus der Kinderen, guus.der.kinderen@gmail.com
*/
public class CertificateUtils
{
/**
* Returns all valid certificates from the provided input, where validity references the notBefore and notAfter
* dates of each certificate.
*
* This method returns all certificates from the input for which {@link X509Certificate#checkValidity()} returns
* true.
*
* The return value of this method is a Set, which means that duplicate certificates in the input are implicitly
* being removed from the result.
*
* @param certificates An array of certificates (possibly empty, possibly null).
* @return A Set of valid certificates (possibly empty, but never null).
*/
public static Set<X509Certificate> filterValid( X509Certificate... certificates )
{
final Set<X509Certificate> results = new HashSet<>();
if (certificates != null)
{
for ( X509Certificate certificate : certificates )
{
if ( certificate == null )
{
continue;
}
try
{
certificate.checkValidity();
}
catch ( CertificateExpiredException | CertificateNotYetValidException e )
{
// Not yet or no longer valid. Don't include in result.
continue;
}
results.add( certificate );
}
}
return results;
}
/**
* Returns all valid certificates from the provided input, where validity references the notBefore and notAfter
* dates of each certificate.
*
* This method returns all certificates from the input for which {@link X509Certificate#checkValidity()} returns
* true.
*
* The return value of this method is a Set, which means that duplicate certificates in the input are implicitly
* being removed from the result.
*
* @param certificates A Collection of certificates (possibly empty, possibly null).
* @return A Set of valid certificates (possibly empty, but never null).
*/
public static Set<X509Certificate> filterValid( Collection<X509Certificate> certificates )
{
if ( certificates == null )
{
return Collections.emptySet();
}
return filterValid( certificates.toArray( new X509Certificate[ certificates.size() ] ) );
}
/**
* Transforms an array of certificates into TrustAnchor instances.
*
* This method does not set the nameConstraints parameter of the generated TrustAnchors.
*
* The return value of this method is a Set, which means that duplicate certificates in the input are implicitly
* being removed from the result.
*
* @param certificates An array of certificates (possibly empty, possibly null).
* @return A Set of valid certificates (possibly empty, but never null).
*/
public static Set<TrustAnchor> toTrustAnchors( X509Certificate... certificates )
{
final Set<TrustAnchor> result = new HashSet<>();
for ( X509Certificate certificate : certificates )
{
if ( certificate == null)
{
continue;
}
result.add( new TrustAnchor( certificate, null ) );
}
return result;
}
/**
* Transforms a collection of certificates into TrustAnchor instances.
*
* This method does not set the nameConstraints parameter of the generated TrustAnchors.
*
* The return value of this method is a Set, which means that duplicate certificates in the input are implicitly
* being removed from the result.
*
* @param certificates An array of certificates (possibly empty, possibly null).
* @return A Set of valid certificates (possibly empty, but never null).
*/
public static Set<TrustAnchor> toTrustAnchors( Collection<X509Certificate> certificates )
{
if ( certificates == null )
{
return Collections.emptySet();
}
return toTrustAnchors( certificates.toArray( new X509Certificate[ certificates.size() ] ) );
}
/**
* Orders certificates, starting from the entity to be validated and progressing back toward the CA root.
*
* This implementation matches "issuers" to "subjects" of certificates in such a way that "issuer" value of a
* certificate matches the "subject" value of the next certificate.
*
* When certificates are provided that do not belong to the same chain, a CertificateException is thrown.
*
* @param certificates an unordered collection of certificates (cannot be null).
* @return An ordered list of certificates (possibly empty, but never null).
*/
public static List<X509Certificate> order( Collection<X509Certificate> certificates ) throws CertificateException
{
final LinkedList<X509Certificate> orderedResult = new LinkedList<>();
if ( certificates.isEmpty() ) {
return orderedResult;
}
if (certificates.size() == 1) {
orderedResult.addAll( certificates );
return orderedResult;
}
final Map<Principal, X509Certificate> byIssuer = new HashMap<>();
final Map<Principal, X509Certificate> bySubject = new HashMap<>();
for ( final X509Certificate certificate : certificates ) {
final Principal issuer = certificate.getIssuerDN();
final Principal subject = certificate.getSubjectDN();
if ( byIssuer.put( issuer, certificate ) != null ) {
throw new CertificateException( "The provided input should not contain multiple certificates with identical issuerDN values." );
}
if ( bySubject.put( subject, certificate ) != null ) {
throw new CertificateException( "The provided input should not contain multiple certificates with identical subjectDN values." );
}
}
// The first certificate will have a 'subject' value that's not an 'issuer' of any other chain.
X509Certificate first = null;
for ( Map.Entry<Principal, X509Certificate> entry : bySubject.entrySet() ) {
final Principal subject = entry.getKey();
final X509Certificate certificate = entry.getValue();
if ( ! byIssuer.containsKey( subject ) ) {
if (first == null) {
first = certificate;
} else {
throw new CertificateException( "The provided input should not contain more than one certificates that has a subjectDN value that's not equal to the issuerDN value of another certificate." );
}
}
}
if (first == null) {
throw new CertificateException( "The provided input should contain a certificates that has a subjectDN value that's not equal to the issuerDN value of any other certificate." );
}
orderedResult.add( first );
// With the first certificate in hand, every following certificate should have a subject that's equal to the previous issuer value.
X509Certificate next = bySubject.get( first.getIssuerDN() );
while (next != null) {
orderedResult.add( next );
next = bySubject.get( next.getIssuerDN() );
}
// final check
if (orderedResult.size() != certificates.size()) {
throw new CertificateException( "Unable to recreate a certificate chain from the provided input." );
}
return orderedResult;
}
/**
* Identifies the End Entity (or 'target') certificate in a chain. In an ordered chain, this is the certificate on
* the opposite end of the CA / Root Certificate.
*
* This implementation can work with incomplete and unordered chains, as long as the provided certificates are all
* part of the same chain (or chain segment). Each certificate in the chain is expected to have issued another
* certificate from the chain, except for one. That one certificate is returned.
*
* This method will throw an exception when no valid chain was provided.
*
* @param chain The chain (possibly incomplete or unordered, but not null, empty or malformed).
* @return The end entity certificate (never null).
* @throws CertificateException When no valid chain was provided.
*/
public static X509Certificate identifyEndEntityCertificate( Collection<X509Certificate> chain ) throws CertificateException
{
final List<X509Certificate> ordered = order( chain );
if (ordered.isEmpty()) {
throw new CertificateException();
}
return ordered.get( 0 );
}
/**
* Attempts to find a point in time on which each of the certificates in the chain will pass
* {@link X509Certificate#checkValidity(Date)}
*
* @param chain The chain for which to find a valid point in time (cannot be null, or empty).
* @return A date on which all certificates in the chain are valid, or null of no such date is available.
*/
public static Date findValidPointInTime( X509Certificate... chain )
{
Date earliestNotAfter = null;
Date latestNotBefore = null;
for ( final X509Certificate certificate : chain )
{
if ( certificate == null ) continue; // ignore nulls.
// Find the earliest 'notAfter'
final Date notAfter = certificate.getNotAfter();
if (earliestNotAfter == null || ( notAfter != null && notAfter.before( earliestNotAfter ) ) )
{
earliestNotAfter = notAfter;
}
// Find the latest 'notBefore'
final Date notBefore = certificate.getNotBefore();
if (latestNotBefore == null || ( notBefore != null && notBefore.after( latestNotBefore ) ) )
{
latestNotBefore = notBefore;
}
}
if ( latestNotBefore != null && earliestNotAfter != null && latestNotBefore.before( earliestNotAfter ) )
{
return latestNotBefore;
}
else
{
// There's no single point in time in which all certificates in this chain are valid.
return null;
}
}
}
......@@ -151,7 +151,7 @@ public class IdentityStoreConfig extends CertificateStoreConfig
}
// Note that PKCS#7 does not require a specific order for the certificates in the file - ordering is needed.
final List<X509Certificate> ordered = CertificateManager.order( certificates );
final List<X509Certificate> ordered = CertificateUtils.order( certificates );
// Of the ordered chain, the first certificate should be for our domain.
if ( !isForThisDomain( ordered.get( 0 ) ) )
......@@ -254,7 +254,7 @@ public class IdentityStoreConfig extends CertificateStoreConfig
}
// Note that PKCS#7 does not require a specific order for the certificates in the file - ordering is needed.
final List<X509Certificate> ordered = CertificateManager.order( certificates );
final List<X509Certificate> ordered = CertificateUtils.order( certificates );
// Of the ordered chain, the first certificate should be for our domain.
if ( !isForThisDomain( ordered.get( 0 ) ) )
......
package org.jivesoftware.openfire.keystore;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.jivesoftware.util.Log;
import javax.net.ssl.*;
import java.security.*;
import java.security.cert.*;
import java.security.cert.Certificate;
import java.util.*;
/**
* A Trust Manager implementation that adds Openfire-proprietary functionality.
*
* @author Guus der Kinderen, guus.der.kinderen@gmail.com
*/
// TODO re-enable optional OCSP checking.
// TODO re-enable CRL checking.
public class OpenfireX509ExtendedTrustManager implements X509TrustManager
{
private static final Provider PROVIDER = new BouncyCastleProvider();
static
{
// Add the BC provider to the list of security providers
Security.addProvider( PROVIDER );
}
/**
* A boolean that indicates if this trust manager will allow self-signed certificates to be trusted.
*/
protected final boolean acceptSelfSigned;
/**
* A boolean that indicates if this trust manager will check if all certificates in the chain (including the root
* certificates) are currently valid (notBefore/notAfter check).
*/
private final boolean checkValidity;
/**
* The set of trusted issuers from the trust store. Note that these certificates are not validated. It is assumed
* that this set can be long-lived. Time-based validation should occur close to the actual usage / invocation.
*/
protected final Set<X509Certificate> trustedIssuers;
public OpenfireX509ExtendedTrustManager( KeyStore trustStore, boolean acceptSelfSigned, boolean checkValidity ) throws NoSuchAlgorithmException, KeyStoreException
{
this.acceptSelfSigned = acceptSelfSigned;
this.checkValidity = checkValidity;
// Retrieve all trusted certificates from the store, but don't validate them just yet!
final Set<X509Certificate> trusted = new HashSet<>();
final Enumeration<String> aliases = trustStore.aliases();
while ( aliases.hasMoreElements() )
{
final String alias = aliases.nextElement();
if ( trustStore.isCertificateEntry( alias ) )
{
final Certificate certificate = trustStore.getCertificate( alias );
if ( certificate instanceof X509Certificate )
{
trusted.add( (X509Certificate) certificate );
}
}
}
trustedIssuers = Collections.unmodifiableSet( trusted );
}
@Override
public void checkClientTrusted( X509Certificate[] chain, String authType ) throws CertificateException
{
// Find and use the end entity as the selector for verification.
final X509Certificate endEntityCert = CertificateUtils.identifyEndEntityCertificate( Arrays.asList( chain ) );
final X509CertSelector selector = new X509CertSelector();
selector.setCertificate( endEntityCert );
try
{
checkChainTrusted( selector, chain );
}
catch ( InvalidAlgorithmParameterException | NoSuchAlgorithmException | CertPathBuilderException ex )
{
throw new CertificateException( ex );
}
}
@Override
public void checkServerTrusted( X509Certificate[] chain, String authType ) throws CertificateException
{
// Find and use the end entity as the selector for verification.
final X509Certificate endEntityCert = CertificateUtils.identifyEndEntityCertificate( Arrays.asList( chain ) );
final X509CertSelector selector = new X509CertSelector();
selector.setCertificate( endEntityCert );
try
{
checkChainTrusted( selector, chain );
}
catch ( InvalidAlgorithmParameterException | NoSuchAlgorithmException | CertPathBuilderException ex )
{
throw new CertificateException( ex );
}
}
@Override
public X509Certificate[] getAcceptedIssuers()
{
final Set<X509Certificate> result;
if ( checkValidity )
{
// Filter the set of issuers to see what certificates are currently valid. Note that this might result in a
// different result as compared with the last verification.
result = CertificateUtils.filterValid( this.trustedIssuers );
}
else
{
result = this.trustedIssuers;
}
return result.toArray( new X509Certificate[ result.size() ] );
}
/**
* Determine if the given partial or complete certificate chain can be trusted to represent the entity that is
* defined by the criteria specified by the 'selector' argument.
*
* A (valid) partial chain is a chain that, combined with certificates from the trust store in this manager, can be
* completed to a full chain.
*
* Chains provided to this method do not need to be in any particular order.
*
* This implementation uses the trust anchors as represented by {@link #getAcceptedIssuers()} to verify that the
* chain that is provided either includes a certificate from an accepted issuer, or is directly issued by one.
*
* Depending on the configuration of this class, other verification is done:
* <ul>
* <li>{@link #acceptSelfSigned}: when <tt>true</tt>, any chain that has a length of one and is self-signed is
* considered as a 'trust anchor' (but is still subject to other checks, such as
* expiration checks).</li>
* </ul>
*
* This method will either return a value, which indicates that the chain is trusted, or will throw an exception.
*
* @param selector Characteristics of the entity to be represented by the chain (cannot be null).
* @param chain The certificate chain that is to be verified (cannot be null or empty).
* @return A trusted certificate path (never null).
*
* @throws InvalidAlgorithmParameterException
* @throws NoSuchAlgorithmException
* @throws CertPathBuilderException
*/
protected CertPath checkChainTrusted( CertSelector selector, X509Certificate... chain ) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, CertPathBuilderException
{
if ( selector == null )
{
throw new IllegalArgumentException( "Argument 'selector' cannot be null");
}
if ( chain == null || chain.length == 0 )
{
throw new IllegalArgumentException( "Argument 'chain' cannot be null or an empty array.");
}
// The set of trusted issuers (for this invocation), based on the issuers from the truststore.
final Set<X509Certificate> trustedIssuers = new HashSet<>();
trustedIssuers.addAll( this.trustedIssuers );
// When accepting self-signed certificates, and the chain is a self-signed certificate, add it to the collection
// of trusted issuers. Blindly accepting this issuer is undesirable, as that would circumvent other checks, such
// as expiration checking.
if ( acceptSelfSigned && chain.length == 1 )
{
final X509Certificate cert = chain[0];
if ( cert.getSubjectDN().equals( cert.getIssuerDN() ) )
{
trustedIssuers.add( cert );
}
}
// Turn trusted into accepted issuers.
final Set<X509Certificate> acceptedIssuers;
if ( checkValidity )
{
// See what certificates are currently valid.
acceptedIssuers = CertificateUtils.filterValid( trustedIssuers );
}
else
{
acceptedIssuers = trustedIssuers;
}
// Transform all accepted issuers into a set of unique trustAnchors.
final Set<TrustAnchor> trustAnchors = CertificateUtils.toTrustAnchors( acceptedIssuers );
// All certificates that are part of the (possibly incomplete) chain.
final CertStore certificates = CertStore.getInstance( "Collection", new CollectionCertStoreParameters( Arrays.asList( chain ) ) );
// Build the configuration for the path builder. It is based on the collection of accepted issuers / trust anchors
final PKIXBuilderParameters parameters = new PKIXBuilderParameters( trustAnchors, selector );
// Validity checks are enabled by default in the CertPathBuilder implementation.
if ( !checkValidity )
{
// There is no way to configure the pathBuilder to ignore date validity. When validity checks are to be
// ignored, try to find a point in time where all certificates in the chain are valid.
final Date validPointInTime = CertificateUtils.findValidPointInTime( chain );
// This strategy to 'disable' validity checks won't work if there's no overlap of validity periods of all
// certificates. TODO improve the implementation.
if ( validPointInTime == null )
{
Log.warn( "The existing implementation is unable to fully ignore certificate validity periods for this chain, even though it is configured to do so. Certificate checks might fail because of expiration for end entity: " + chain[0] );
}
else
{
parameters.setDate( validPointInTime );
}
}
// Add all certificates that are part of the chain to the configuration. Together with the trust anchors, the
// entire chain should now be in the store.
parameters.addCertStore( certificates );
// Finally, construct (and implicitly validate) the certificate path.
final CertPathBuilder pathBuilder = CertPathBuilder.getInstance( "PKIX" );
final CertPathBuilderResult result = pathBuilder.build( parameters );
return result.getCertPath();
}
}
package org.jivesoftware.openfire.keystore;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.jivesoftware.openfire.net.ClientTrustManager;
import org.jivesoftware.openfire.net.ServerTrustManager;
import org.jivesoftware.util.CertificateManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
......@@ -28,142 +30,67 @@ public class TrustStoreConfig extends CertificateStoreConfig
{
private static final Logger Log = LoggerFactory.getLogger( TrustStoreConfig.class );
private final TrustManagerFactory trustFactory;
private transient TrustManager[] trustManagers;
private final CertPathValidator certPathValidator; // not thread safe
private final CertificateFactory certificateFactory; // not thread safe.
private boolean acceptSelfSigned;
private boolean checkValidity;
public TrustStoreConfig( String path, String password, String type, boolean createIfAbsent ) throws CertificateStoreConfigException
public TrustStoreConfig( String path, String password, String type, boolean createIfAbsent, boolean acceptSelfSigned, boolean checkValidity ) throws CertificateStoreConfigException
{
super( path, password, type, createIfAbsent );
try
{
certPathValidator = CertPathValidator.getInstance( "PKIX" );
certificateFactory = CertificateFactory.getInstance( "X.509" );
trustFactory = TrustManagerFactory.getInstance( TrustManagerFactory.getDefaultAlgorithm() );
trustFactory.init( store );
}
catch ( CertificateException | NoSuchAlgorithmException | KeyStoreException ex )
{
throw new CertificateStoreConfigException( "Unable to load store of type '" + type + "' from location '" + path + "'", ex );
}
this.acceptSelfSigned = acceptSelfSigned;
this.checkValidity = checkValidity;
}
public TrustManager[] getTrustManagers()
{
return trustFactory.getTrustManagers();
}
/**
* Returns all valid certificates from the store.
*
* @return A collection of certificates (possibly empty, but never null).
*/
protected Set<TrustAnchor> getAllValidTrustAnchors() throws KeyStoreException
public synchronized TrustManager[] getTrustManagers() throws KeyStoreException, NoSuchAlgorithmException
{
final Set<TrustAnchor> results = new HashSet<>();
for ( X509Certificate certificate : getAllCertificates().values() )
{
try
{
certificate.checkValidity();
}
catch ( CertificateExpiredException | CertificateNotYetValidException e )
{
// Not yet or no longer valid. Don't include in result.
continue;
}
final TrustAnchor trustAnchor = new TrustAnchor( certificate, null );
results.add( trustAnchor );
if ( trustManagers == null ) {
trustManagers = new TrustManager[] { new OpenfireX509ExtendedTrustManager( this.getStore(), acceptSelfSigned, checkValidity ) };
}
return results;
return trustManagers;
}
/**
* Validates the provided certificate chain, by verifying (among others):
* <ul>
* <li>The validity of each certificate in the chain</li>
* <li>chain integrity (matching issuer/subject)</li>
* <li>the root of the chain is validated by a trust anchor that is in this store.</li>
* </ul>
*
* @param chain A chain of certificates (cannot be null)
* @return true when the validity of the chain could be verified, otherwise false.
*/
public synchronized boolean canTrust( Collection<X509Certificate> chain )
public synchronized void reconfigure( boolean acceptSelfSigned, boolean checkValidity ) throws CertificateStoreConfigException
{
// Input validation
if ( chain == null )
boolean needsReload = false;
if ( this.acceptSelfSigned != acceptSelfSigned )
{
throw new IllegalArgumentException( "Argument 'chain' cannot be null." );
this.acceptSelfSigned = acceptSelfSigned;
needsReload = true;
}
if (chain.isEmpty() )
if ( this.checkValidity != checkValidity )
{
return false;
this.checkValidity = checkValidity;
needsReload = true;
}
// For some reason, the default validation fails to iterate over all providers and will fail if the default
// provider does not support the algorithm of the chain. To work around this issue, this code iterates over
// each provider explicitly, returning success when at least one provider validates the chain successfully.
Log.debug( "Iterating over all available security providers in order to validate a certificate chain." );
for (Provider p : Security.getProviders())
{
try
{
final Set<TrustAnchor> trustAnchors = getAllValidTrustAnchors();
final CertPath certPath = getCertPath( chain );
final PKIXParameters parameters = new PKIXParameters( trustAnchors );
parameters.setRevocationEnabled( false ); // TODO: enable revocation list validation.
parameters.setSigProvider( p.getName() ); // Explicitly iterate over each signature provider. See comment above.
certPathValidator.validate( certPath, parameters );
Log.debug( "Provider "+p.getName()+": Able to validate certificate chain." );
return true;
}
catch ( Exception ex )
{
Log.debug( "Provider "+p.getName()+": Unable to validate certificate chain.", ex );
}
if ( needsReload ) {
reload();
}
return false;
}
/**
* Creates a CertPath instance from the provided certificate chain.
*
* This implementation can process unordered input (ordering will by applied).
*
* @param chain A certificate chain (cannot be null or an empty collection).
* @return A CertPath instance (never null).
* @throws CertificateException When no CertPath instance could be created.
*/
protected synchronized CertPath getCertPath( Collection<X509Certificate> chain ) throws CertificateException
public boolean isAcceptSelfSigned()
{
// Input validation
if ( chain == null || chain.isEmpty() )
{
throw new IllegalArgumentException( "Argument 'chain' cannot be null or empty." );
}
return acceptSelfSigned;
}
// Note that PKCS#7 does not require a specific order for the certificates in the file - ordering is needed.
final List<X509Certificate> ordered = CertificateManager.order( chain );
public boolean isCheckValidity()
{
return checkValidity;
}
return certificateFactory.generateCertPath( ordered );
@Override
public synchronized void reload() throws CertificateStoreConfigException
{
super.reload();
trustManagers = null;
}
/**
* Imports one certificate as a trust anchor into this store.
*
* Note that this method explicitly allows one to add invalid certificates. Other methods in this class might ignore
* such a certificate ({@link #canTrust(Collection)} being a prime example).
* Note that this method explicitly allows one to add invalid certificates.
*
* As this store is intended to contain certificates for "most-trusted" / root Certificate Authorities, this method
* will fail when the PEM representation contains more than one certificate.
......@@ -209,9 +136,12 @@ public class TrustStoreConfig extends CertificateStoreConfig
}
catch ( CertificateException | KeyStoreException | IOException e )
{
reload(); // reset state of the store.
throw new CertificateStoreConfigException( "Unable to install a certificate into a trust store.", e );
}
finally
{
reload(); // re-initialize store.
}
// TODO Notify listeners that a new certificate has been added.
}
......
......@@ -20,20 +20,23 @@
package org.jivesoftware.openfire.net;
import org.apache.mina.filter.ssl.SslFilter;
import org.jivesoftware.openfire.Connection;
import org.jivesoftware.openfire.keystore.*;
import org.jivesoftware.openfire.session.ConnectionSettings;
import org.jivesoftware.util.JiveGlobals;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.TrustManager;
import java.io.*;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.*;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
......@@ -62,6 +65,7 @@ public class SSLConfig
catch ( CertificateStoreConfigException | NoSuchAlgorithmException | IOException ex )
{
Log.error( "Unable to instantiate SSL Configuration!", ex );
ex.printStackTrace();
}
}
......@@ -245,7 +249,9 @@ public class SSLConfig
if ( !storesByLocation.containsKey( location )) {
final CertificateStoreConfig storeConfig;
if (purpose.isTrustStore()) {
storeConfig = new TrustStoreConfig( getLocation( purpose ), getPassword( purpose ), getStoreType( purpose ), false );
final boolean acceptSelfSigned = false; // TODO make configurable
final boolean checkValidity = true; ; // TODO make configurable
storeConfig = new TrustStoreConfig( getLocation( purpose ), getPassword( purpose ), getStoreType( purpose ), false, acceptSelfSigned, checkValidity );
} else {
storeConfig = new IdentityStoreConfig( getLocation( purpose ), getPassword( purpose ), getStoreType( purpose ), false );
}
......@@ -264,117 +270,430 @@ public class SSLConfig
return storesByLocation.get( locationByPurpose.get( purpose ) );
}
public void useStoreForPurpose( Purpose purpose, String location, String password, String storeType, boolean createIfAbsent ) throws IOException, CertificateStoreConfigException
{
final String newPath = canonicalize( location );
final String oldPath = locationByPurpose.get( purpose );
final CertificateStoreConfig oldConfig = storesByLocation.get( oldPath );
// public void useStoreForPurpose( Purpose purpose, String location, String password, String storeType, boolean createIfAbsent ) throws IOException, CertificateStoreConfigException
// {
// final String newPath = canonicalize( location );
// final String oldPath = locationByPurpose.get( purpose );
// final CertificateStoreConfig oldConfig = storesByLocation.get( oldPath );
//
// // When this invocation does not change the current state, only trigger a reload.
// if (oldPath.equalsIgnoreCase( newPath ) && oldConfig.getPassword().equals( password ) && oldConfig.getType().equals( storeType ))
// {
// oldConfig.reload();
// return;
// }
//
// // Has a store already been loaded from this location?
// final boolean isKnown = storesByLocation.containsKey( newPath );
//
// final CertificateStoreConfig newConfig;
// if ( isKnown )
// {
// newConfig = storesByLocation.get( newPath );
// }
// else
// {
// if (purpose.isTrustStore()) {
// final boolean acceptSelfSigned = false; // TODO make configurable
// final boolean checkValidity = true; ; // TODO make configurable
// newConfig = new TrustStoreConfig( newPath, password, storeType, createIfAbsent, acceptSelfSigned, checkValidity );
// } else {
// newConfig = new IdentityStoreConfig( newPath, password, storeType, createIfAbsent );
// }
// }
//
// locationByPurpose.replace( purpose, newConfig.getCanonicalPath() );
// storesByLocation.replace( newConfig.getCanonicalPath(), newConfig );
//
// // Persist changes by modifying the Openfire properties.
// final Path locationToStore = Paths.get( newConfig.getPath() );
//
// switch ( purpose )
// {
// case SOCKETBASED_IDENTITYSTORE:
// JiveGlobals.setProperty( "xmpp.socket.ssl.keystore", locationToStore.toString() );
// JiveGlobals.setProperty( "xmpp.socket.ssl.keypass", password );
// JiveGlobals.setProperty( "xmpp.socket.ssl.storeType", storeType ); // FIXME also in use by SOCKETBASED_S2S_TRUSTSTORE
// break;
//
// case BOSHBASED_IDENTITYSTORE:
// JiveGlobals.setProperty( "xmpp.bosh.ssl.keystore", locationToStore.toString() );
// JiveGlobals.setProperty( "xmpp.bosh.ssl.keypass", password );
// JiveGlobals.setProperty( "xmpp.bosh.ssl.storeType", storeType );
// break;
//
// case ADMINISTRATIVE_IDENTITYSTORE:
// JiveGlobals.setProperty( "admin.ssl.keystore", locationToStore.toString() );
// JiveGlobals.setProperty( "admin.ssl.keypass", password );
// JiveGlobals.setProperty( "admin.ssl.storeType", storeType ); // FIXME also in use by ADMINISTRATIVE_TRUSTSTORE
// break;
//
// case WEBADMIN_IDENTITYSTORE:
// JiveGlobals.setProperty( "admin.web.ssl.keystore", locationToStore.toString() );
// JiveGlobals.setProperty( "admin.web.ssl.keypass", password );
// JiveGlobals.setProperty( "admin.web.ssl.storeType", storeType ); // FIXME also in use by WEBADMIN_TRUSTSTORE
// break;
//
// case SOCKETBASED_S2S_TRUSTSTORE:
// JiveGlobals.setProperty( "xmpp.socket.ssl.truststore", locationToStore.toString() );
// JiveGlobals.setProperty( "xmpp.socket.ssl.trustpass", password );
// JiveGlobals.setProperty( "xmpp.socket.ssl.storeType", storeType ); // FIXME also in use by SOCKETBASED_IDENTITYSTORE
// break;
//
// case SOCKETBASED_C2S_TRUSTSTORE:
// JiveGlobals.setProperty( "xmpp.socket.ssl.client.truststore", locationToStore.toString() );
// JiveGlobals.setProperty( "xmpp.socket.ssl.client.trustpass", password );
// JiveGlobals.setProperty( "xmpp.socket.ssl.client.storeType", storeType );
// break;
//
// case BOSHBASED_C2S_TRUSTSTORE:
// JiveGlobals.setProperty( "xmpp.bosh.ssl.client.truststore", locationToStore.toString() );
// JiveGlobals.setProperty( "xmpp.bosh.ssl.client.trustpass", password );
// JiveGlobals.setProperty( "xmpp.bosh.ssl.storeType", storeType );
// break;
//
// case ADMINISTRATIVE_TRUSTSTORE:
// JiveGlobals.setProperty( "admin.ssl.truststore", locationToStore.toString() );
// JiveGlobals.setProperty( "admin.ssl.trustpass", password );
// JiveGlobals.setProperty( "admin.ssl.storeType", storeType ); // FIXME also in use by ADMINISTRATIVE_IDENTITYSTORE
//
// case WEBADMIN_TRUSTSTORE:
// JiveGlobals.setProperty( "admin.web.ssl.truststore", locationToStore.toString() );
// JiveGlobals.setProperty( "admin.web.ssl.trustpass", password );
// JiveGlobals.setProperty( "admin.web.ssl.storeType", storeType ); // FIXME also in use by WEBADMIN_IDENTITYSTORE
//
// default:
// throw new IllegalStateException( "Unrecognized purpose: " + purpose );
// }
//
// // TODO notify listeners
// }
// When this invocation does not change the current state, only trigger a reload.
if (oldPath.equalsIgnoreCase( newPath ) && oldConfig.getPassword().equals( password ) && oldConfig.getType().equals( storeType ))
{
oldConfig.reload();
return;
public static String canonicalize( String path ) throws IOException
{
File file = new File( path );
if (!file.isAbsolute()) {
file = new File( JiveGlobals.getHomeDirectory() + File.separator + path );
}
// Has a store already been loaded from this location?
final boolean isKnown = storesByLocation.containsKey( newPath );
return file.getCanonicalPath();
}
final CertificateStoreConfig newConfig;
if ( isKnown )
{
newConfig = storesByLocation.get( newPath );
// TODO merge this with Purpose!
public enum Type {
SOCKET_S2S( "xmpp.socket.ssl.", null ),
SOCKET_C2S( "xmpp.socket.ssl.client.", null ),
BOSH_C2S( "xmpp.bosh.ssl.client.", SOCKET_C2S),
ADMIN( "admin.ssl.", SOCKET_S2S),
WEBADMIN( "admin.web.ssl.", ADMIN);
String prefix;
Type fallback;
Type( String prefix, Type fallback) {
this.prefix = prefix;
this.fallback = fallback;
}
else
public String getPrefix()
{
if (purpose.isTrustStore()) {
newConfig = new TrustStoreConfig( newPath, password, storeType, createIfAbsent );
} else {
newConfig = new IdentityStoreConfig( newPath, password, storeType, createIfAbsent );
}
return prefix;
}
locationByPurpose.replace( purpose, newConfig.getCanonicalPath() );
storesByLocation.replace( newConfig.getCanonicalPath(), newConfig );
public Type getFallback()
{
return fallback;
}
}
// Persist changes by modifying the Openfire properties.
final Path locationToStore = Paths.get( newConfig.getPath() );
public static SSLContext getSSLContext( final Type type ) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException
{
// TODO: allow different algorithms for different connection types (eg client/server/bosh etc)
final String algorithm = JiveGlobals.getProperty( ConnectionSettings.Client.TLS_ALGORITHM, "TLS" );
final Purpose idPurpose;
final Purpose trustPurpose;
switch ( type ) {
case SOCKET_S2S:
idPurpose = Purpose.SOCKETBASED_IDENTITYSTORE;
trustPurpose = Purpose.SOCKETBASED_S2S_TRUSTSTORE;
break;
switch ( purpose )
{
case SOCKETBASED_IDENTITYSTORE:
JiveGlobals.setProperty( "xmpp.socket.ssl.keystore", locationToStore.toString() );
JiveGlobals.setProperty( "xmpp.socket.ssl.keypass", password );
JiveGlobals.setProperty( "xmpp.socket.ssl.storeType", storeType ); // FIXME also in use by SOCKETBASED_S2S_TRUSTSTORE
case SOCKET_C2S:
idPurpose = Purpose.SOCKETBASED_IDENTITYSTORE;
trustPurpose = Purpose.SOCKETBASED_C2S_TRUSTSTORE;
break;
case BOSHBASED_IDENTITYSTORE:
JiveGlobals.setProperty( "xmpp.bosh.ssl.keystore", locationToStore.toString() );
JiveGlobals.setProperty( "xmpp.bosh.ssl.keypass", password );
JiveGlobals.setProperty( "xmpp.bosh.ssl.storeType", storeType );
case BOSH_C2S:
idPurpose = Purpose.BOSHBASED_IDENTITYSTORE;
trustPurpose = Purpose.BOSHBASED_C2S_TRUSTSTORE;
break;
case ADMINISTRATIVE_IDENTITYSTORE:
JiveGlobals.setProperty( "admin.ssl.keystore", locationToStore.toString() );
JiveGlobals.setProperty( "admin.ssl.keypass", password );
JiveGlobals.setProperty( "admin.ssl.storeType", storeType ); // FIXME also in use by ADMINISTRATIVE_TRUSTSTORE
case ADMIN:
idPurpose = Purpose.ADMINISTRATIVE_IDENTITYSTORE;
trustPurpose = Purpose.ADMINISTRATIVE_TRUSTSTORE;
break;
case WEBADMIN_IDENTITYSTORE:
JiveGlobals.setProperty( "admin.web.ssl.keystore", locationToStore.toString() );
JiveGlobals.setProperty( "admin.web.ssl.keypass", password );
JiveGlobals.setProperty( "admin.web.ssl.storeType", storeType ); // FIXME also in use by WEBADMIN_TRUSTSTORE
case WEBADMIN:
idPurpose = Purpose.WEBADMIN_IDENTITYSTORE;
trustPurpose = Purpose.WEBADMIN_TRUSTSTORE;
break;
case SOCKETBASED_S2S_TRUSTSTORE:
JiveGlobals.setProperty( "xmpp.socket.ssl.truststore", locationToStore.toString() );
JiveGlobals.setProperty( "xmpp.socket.ssl.trustpass", password );
JiveGlobals.setProperty( "xmpp.socket.ssl.storeType", storeType ); // FIXME also in use by SOCKETBASED_IDENTITYSTORE
default:
throw new IllegalStateException( "Unsupported type: " + type );
}
final KeyManager[] keyManagers = ((IdentityStoreConfig) getInstance().getStoreConfig( idPurpose )).getKeyManagers();
final TrustManager[] trustManagers = ((TrustStoreConfig) getInstance().getStoreConfig( trustPurpose )).getTrustManagers();
final SSLContext sslContext = SSLContext.getInstance( algorithm );
sslContext.init( keyManagers, trustManagers, new SecureRandom() );
return sslContext;
}
/**
* Creates an SSL Engine that is configured to use server mode when handshaking.
*
* For Openfire, an engine is of this mode used for most purposes (as Openfire is a server by nature).
*
* @param type The type of connectivity for which to configure a new SSLEngine instance. Cannot be null.
* @param clientAuth indication of the desired level of client-sided authentication (mutual authentication). Cannot be null.
* @return An initialized SSLEngine instance (never null).
*/
public static SSLEngine getServerModeSSLEngine( Type type, Connection.ClientAuth clientAuth ) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException
{
final SSLEngine sslEngine = getSSLEngine( type );
sslEngine.setUseClientMode( false );
switch ( clientAuth )
{
case needed:
sslEngine.setNeedClientAuth( true );
break;
case SOCKETBASED_C2S_TRUSTSTORE:
JiveGlobals.setProperty( "xmpp.socket.ssl.client.truststore", locationToStore.toString() );
JiveGlobals.setProperty( "xmpp.socket.ssl.client.trustpass", password );
JiveGlobals.setProperty( "xmpp.socket.ssl.client.storeType", storeType );
case wanted:
sslEngine.setWantClientAuth( true );
break;
case BOSHBASED_C2S_TRUSTSTORE:
JiveGlobals.setProperty( "xmpp.bosh.ssl.client.truststore", locationToStore.toString() );
JiveGlobals.setProperty( "xmpp.bosh.ssl.client.trustpass", password );
JiveGlobals.setProperty( "xmpp.bosh.ssl.storeType", storeType );
case disabled:
sslEngine.setWantClientAuth( false );
break;
}
case ADMINISTRATIVE_TRUSTSTORE:
JiveGlobals.setProperty( "admin.ssl.truststore", locationToStore.toString() );
JiveGlobals.setProperty( "admin.ssl.trustpass", password );
JiveGlobals.setProperty( "admin.ssl.storeType", storeType ); // FIXME also in use by ADMINISTRATIVE_IDENTITYSTORE
return sslEngine;
}
case WEBADMIN_TRUSTSTORE:
JiveGlobals.setProperty( "admin.web.ssl.truststore", locationToStore.toString() );
JiveGlobals.setProperty( "admin.web.ssl.trustpass", password );
JiveGlobals.setProperty( "admin.web.ssl.storeType", storeType ); // FIXME also in use by WEBADMIN_IDENTITYSTORE
/**
* Creates an SSL Engine that is configured to use client mode when handshaking.
*
* For Openfire, an engine of this mode is typically used when the server tries to connect to another server.
*
* @param type The type of connectivity for which to configure a new SSLEngine instance. Cannot be null.
* @return An initialized SSLEngine instance (never null).
*/
public static SSLEngine getClientModeSSLEngine( Type type ) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException
{
final SSLEngine sslEngine = getSSLEngine( type );
sslEngine.setUseClientMode( true );
default:
throw new IllegalStateException( "Unrecognized purpose: " + purpose );
return sslEngine;
}
/**
* A utility method that implements the shared functionality of getClientModeSSLEngine and getServerModeSSLEngine.
*
* This method is used to initialize and pre-configure an instance of SSLEngine for a particular connection type.
* The returned value lacks further configuration. In most cases, developers will want to use getClientModeSSLEngine
* or getServerModeSSLEngine instead of this method.
*
* @param type The type of connectivity for which to pre-configure a new SSLEngine instance. Cannot be null.
* @return A pre-configured SSLEngine (never null).
*/
private static SSLEngine getSSLEngine( final Type type ) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException
{
final SSLContext sslContext = getSSLContext( type );
final SSLEngine sslEngine = sslContext.createSSLEngine();
configureProtocols( sslEngine, type );
configureCipherSuites( sslEngine, type );
return sslEngine;
}
/**
* Enables a specific set of protocols in an SSLEngine instance.
*
* To determine what protocols to enable, this implementation first looks at a type-specific property. This property
* can contain a comma-separated list of protocols that are to be enabled.
*
* When the property is not set (or when the property value is empty), the protocols that will be enabled are all
* protocols supported by the SSLEngine for which the protocol name starts with "TLS".
*
* Note that the selection strategy is a different strategy than with cipher suites in configureCipherSuites(),
* where the SSLEngine default gets filtered but not replaced.
*
* @param sslEngine The instance to configure. Cannot be null.
* @param type The type of configuration to use (used to select the relevent property). Cannot be null.
*/
private static void configureProtocols( SSLEngine sslEngine, Type type )
{
// Find configuration, using fallback where applicable.
String enabledProtocols = JiveGlobals.getProperty( type.getPrefix() + "enabled.protocols" );
while (enabledProtocols == null && type.getFallback() != null)
{
type = type.getFallback();
enabledProtocols = JiveGlobals.getProperty( type.getPrefix() + "enabled.protocols" );
}
// TODO notify listeners
if (enabledProtocols != null )
{
final String[] protocols = enabledProtocols.split( "," );
if (protocols != null && protocols.length > 0)
{
sslEngine.setEnabledProtocols( protocols );
}
}
else
{
// When no user-based configuration is available, the SSL Engine will use a default. Instead of this default,
// we want all of the TLS protocols that are supported (which will exclude all of the older, insecure SSL
// protocols).
final ArrayList<String> defaultEnabled = new ArrayList<>();
for ( String supported : sslEngine.getSupportedProtocols() )
{
// Include only TLS protocols.
if ( supported.toUpperCase().startsWith( "TLS" ) )
{
defaultEnabled.add( supported );
}
}
sslEngine.setEnabledProtocols( defaultEnabled.toArray( new String[ defaultEnabled.size()] ) );
}
}
public static String canonicalize( String path ) throws IOException
/**
* Enables a specific set of cipher suites in an SSLEngine instance.
*
* To determine what suites to enable, this implementation first looks at a type-specific property. This property
* can contain a comma-separated list of suites that are to be enabled.
*
* When the property is not set (or when the property value is empty), the suites that will be enabled are all
* suites that are enabled by default in the SSLEngine, with the exclusion of a number of known weak suites.
*
* Note that the selection strategy is a different strategy than with protocols in configureProtocols(), where the
* entire SSLEngine default gets replaced.
*
* @param sslEngine The instance to configure. Cannot be null.
* @param type The type of configuration to use (used to select the relevent property). Cannot be null.
*/
private static void configureCipherSuites( SSLEngine sslEngine, Type type )
{
File file = new File( path );
if (!file.isAbsolute()) {
file = new File( JiveGlobals.getHomeDirectory() + File.separator + path );
String enabledCipherSuites = JiveGlobals.getProperty( type.getPrefix() + "enabled.ciphersuites" );
while (enabledCipherSuites == null && type.getFallback() != null)
{
type = type.getFallback();
enabledCipherSuites = JiveGlobals.getProperty( type.getPrefix() + "enabled.ciphersuites" );
}
return file.getCanonicalPath();
if (enabledCipherSuites != null )
{
final String[] suites = enabledCipherSuites.split( "," );
if (suites != null && suites.length > 0)
{
sslEngine.setEnabledCipherSuites( suites );
}
}
else
{
// When no user-based configuration is available, the SSL Engine will use a default. From this default, we
// want to filter out a couple of insecure ciphers.
final ArrayList<String> defaultEnabled = new ArrayList<>();
for ( String supported : sslEngine.getSupportedCipherSuites() )
{
// A number of weaknesses in SHA-1 are known. It is no longer recommended to be used.
if ( supported.toUpperCase().endsWith( "SHA" ) )
{
continue;
}
// Due to problems with collision resistance MD5 is no longer safe to use.
if ( supported.toUpperCase().endsWith( "MD5" ) )
{
continue;
}
defaultEnabled.add( supported );
}
sslEngine.setEnabledCipherSuites( defaultEnabled.toArray( new String[ defaultEnabled.size()] ) );
}
}
public static SSLContext getSSLContext() throws NoSuchAlgorithmException
/**
* Creates an Apache MINA SslFilter that is configured to use server mode when handshaking.
*
* For Openfire, an engine is of this mode used for most purposes (as Openfire is a server by nature).
*
* Instead of an SSLContext or SSLEngine, Apache MINA uses an SslFilter instance. It is generally not needed to
* create both SSLContext/SSLEngine as well as SslFilter instances.
*
* @param type Communication type (used to select the relevant property). Cannot be null.
* @param clientAuth indication of the desired level of client-sided authentication (mutual authentication). Cannot be null.
* @return An initialized SslFilter instance (never null)
*/
public static SslFilter getServerModeSslFilter( SSLConfig.Type type, Connection.ClientAuth clientAuth ) throws KeyManagementException, NoSuchAlgorithmException, KeyStoreException
{
// TODO: allow different algorithms for differetn connection types (eg client/server/bosh etc)
final String algorithm = JiveGlobals.getProperty( ConnectionSettings.Client.TLS_ALGORITHM, "TLS" );
return SSLContext.getInstance( algorithm );
final SSLContext sslContext = SSLConfig.getSSLContext( type );
final SSLEngine sslEngine = SSLConfig.getServerModeSSLEngine( type, clientAuth );
return getSslFilter( sslContext, sslEngine );
}
/**
* Creates an Apache MINA SslFilter that is configured to use client mode when handshaking.
*
* For Openfire, a filter of this mode is typically used when the server tries to connect to another server.
*
* Instead of an SSLContext or SSLEngine, Apache MINA uses an SslFilter instance. It is generally not needed to
* create both SSLContext/SSLEngine as well as SslFilter instances.
*
* @param type Communication type (used to select the relevant property). Cannot be null.
* @return An initialized SslFilter instance (never null)
*/
public static SslFilter getClientModeSslFilter( SSLConfig.Type type ) throws KeyManagementException, NoSuchAlgorithmException, KeyStoreException
{
final SSLContext sslContext = SSLConfig.getSSLContext( type );
final SSLEngine sslEngine = SSLConfig.getClientModeSSLEngine( type );
return getSslFilter( sslContext, sslEngine );
}
/**
* A utility method that implements the shared functionality of getServerModeSslFilter and getClientModeSslFilter.
*
* This method is used to initialize and configure an instance of SslFilter for a particular pre-configured
* SSLContext and SSLEngine. In most cases, developers will want to use getServerModeSslFilter or
* getClientModeSslFilter instead of this method.
*
* @param sslContext a pre-configured SSL Context instance (cannot be null).
* @param sslEngine a pre-configured SSL Engine instance (cannot be null).
* @return A SslFilter instance (never null).
*/
private static SslFilter getSslFilter( SSLContext sslContext, SSLEngine sslEngine ) {
final SslFilter filter = new SslFilter( sslContext );
// Copy configuration from the SSL Engine into the filter.
filter.setUseClientMode( sslEngine.getUseClientMode() );
filter.setEnabledProtocols( sslEngine.getEnabledProtocols() );
filter.setEnabledCipherSuites( sslEngine.getEnabledCipherSuites() );
// Note that the setters for 'need' and 'want' influence each-other. Invoke only one of them!
if ( sslEngine.getNeedClientAuth() ) {
filter.setNeedClientAuth( true );
} else if ( sslEngine.getWantClientAuth() ) {
filter.setWantClientAuth( true );
}
return filter;
}
}
......@@ -168,16 +168,24 @@ public class SocketConnection implements Connection {
@Deprecated
public void startTLS(boolean clientMode, String remoteServer, ClientAuth authentication) throws Exception {
final boolean isClientToServer = ( remoteServer == null );
startTLS( clientMode, isClientToServer, authentication );
final boolean isPeerClient = ( remoteServer == null );
startTLS( clientMode, isPeerClient, authentication );
}
public void startTLS(boolean clientMode, boolean isClientToServer, ClientAuth authentication) throws IOException {
public void startTLS(boolean clientMode, boolean isPeerClient, ClientAuth authentication) throws IOException {
if (!secure) {
secure = true;
// Prepare for TLS
tlsStreamHandler = new TLSStreamHandler(this, socket, clientMode, remoteServer,
session instanceof IncomingServerSession);
final ClientAuth clientAuth;
if (session instanceof IncomingServerSession)
{
clientAuth = ClientAuth.needed;
}
else
{
clientAuth = ClientAuth.wanted;
}
tlsStreamHandler = new TLSStreamHandler(socket, clientMode, isPeerClient, clientAuth);
if (!clientMode) {
// Indicate the client that the server is ready to negotiate TLS
deliverRawText("<proceed xmlns=\"urn:ietf:params:xml:ns:xmpp-tls\"/>");
......@@ -574,7 +582,7 @@ public class SocketConnection implements Connection {
private void release() {
writeStarted = -1;
instances.remove(this);
}
}
private void closeConnection() {
release();
......
......@@ -94,7 +94,12 @@ public class TLSStreamHandler {
public TLSStreamHandler(Connection connection, Socket socket, boolean clientMode, String remoteServer,
boolean needClientAuth) throws IOException
{
this(socket,clientMode,(remoteServer==null),needClientAuth);
this(
socket,
clientMode,
remoteServer==null,
needClientAuth?Connection.ClientAuth.needed: Connection.ClientAuth.wanted
);
}
/**
......@@ -105,14 +110,12 @@ public class TLSStreamHandler {
*
* @param socket the plain socket connection to secure
* @param clientMode boolean indicating if this entity is a client or a server.
* @param isClientToServer indicates if the remote party is a server.
* @param needClientAuth boolean that indicates if client should authenticate during the TLS
* negotiation. This option is only required when the client is a server since
* EXTERNAL SASL is going to be used.
* @param isPeerClient indicates if the remote party is a client (or server).
* @param clientAuth indicates if client should authenticate during the TLS negotiation.
* @throws java.io.IOException
*/
public TLSStreamHandler(Socket socket, boolean clientMode, boolean isClientToServer, boolean needClientAuth) throws IOException {
wrapper = new TLSWrapper(clientMode, needClientAuth, isClientToServer);
public TLSStreamHandler(Socket socket, boolean clientMode, boolean isPeerClient, Connection.ClientAuth clientAuth) throws IOException {
wrapper = new TLSWrapper(clientMode, clientAuth, isPeerClient);
tlsEngine = wrapper.getTlsEngine();
reader = new TLSStreamReader(wrapper, socket);
writer = new TLSStreamWriter(wrapper, socket);
......@@ -145,7 +148,7 @@ public class TLSStreamHandler {
initialHSStatus = HandshakeStatus.NEED_WRAP;
tlsEngine.beginHandshake();
}
else if (needClientAuth) {
else if (clientAuth == Connection.ClientAuth.needed) {
// Only REQUIRE client authentication if we are fully verifying certificates
if (JiveGlobals.getBooleanProperty(ConnectionSettings.Server.TLS_CERTIFICATE_VERIFY, true) &&
JiveGlobals.getBooleanProperty(ConnectionSettings.Server.TLS_CERTIFICATE_CHAIN_VERIFY, true) &&
......
......@@ -23,21 +23,14 @@ package org.jivesoftware.openfire.net;
import java.nio.ByteBuffer;
import java.security.*;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLEngineResult;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.SSLEngineResult.HandshakeStatus;
import javax.net.ssl.SSLEngineResult.Status;
import org.jivesoftware.openfire.Connection;
import org.jivesoftware.openfire.keystore.IdentityStoreConfig;
import org.jivesoftware.openfire.keystore.Purpose;
import org.jivesoftware.openfire.keystore.TrustStoreConfig;
import org.jivesoftware.openfire.session.ConnectionSettings;
import org.jivesoftware.util.JiveGlobals;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
......@@ -70,59 +63,34 @@ public class TLSWrapper {
@Deprecated
public TLSWrapper(Connection connection, boolean clientMode, boolean needClientAuth, String remoteServer)
{
this(clientMode,needClientAuth,(remoteServer == null));
this(
clientMode,
needClientAuth?Connection.ClientAuth.needed:Connection.ClientAuth.wanted,
remoteServer == null
);
}
public TLSWrapper(boolean clientMode, boolean needClientAuth, boolean isClientToServer) {
public TLSWrapper(boolean clientMode, Connection.ClientAuth clientAuth, boolean isPeerClient) {
// Create/initialize the SSLContext with key material
try {
// First initialize the key and trust material.
final SSLConfig sslConfig = SSLConfig.getInstance();
final Purpose purpose = (isClientToServer ? Purpose.SOCKETBASED_C2S_TRUSTSTORE : Purpose.SOCKETBASED_S2S_TRUSTSTORE );
final TrustStoreConfig trustStoreConfig = (TrustStoreConfig) sslConfig.getStoreConfig( purpose );
// TrustManager's decide whether to allow connections.
final TrustManager[] tm;
if (clientMode || needClientAuth)
try
{
final SSLEngine sslEngine;
if ( clientMode )
{
final KeyStore ksTrust = trustStoreConfig.getStore();
if (isClientToServer)
{
// Check if we can trust certificates presented by the client
tm = new TrustManager[]{new ClientTrustManager(ksTrust)};
}
else
{
// Check if we can trust certificates presented by the server
tm = new TrustManager[]{new ServerTrustManager(ksTrust)};
}
sslEngine = SSLConfig.getClientModeSSLEngine( SSLConfig.Type.SOCKET_S2S );
}
else
{
tm = trustStoreConfig.getTrustManagers();
final SSLConfig.Type type = isPeerClient ? SSLConfig.Type.SOCKET_C2S : SSLConfig.Type.SOCKET_S2S;
sslEngine = SSLConfig.getServerModeSSLEngine( type, clientAuth );
}
final IdentityStoreConfig identityStoreConfig = (IdentityStoreConfig) sslConfig.getStoreConfig( Purpose.SOCKETBASED_IDENTITYSTORE );
final SSLContext tlsContext = SSLConfig.getSSLContext();
tlsContext.init( identityStoreConfig.getKeyManagers(), tm, null);
/*
* Configure the tlsEngine to act as a server in the SSL/TLS handshake. We're a server,
* so no need to use host/port variant.
*
* The first call for a server is a NEED_UNWRAP.
*/
tlsEngine = tlsContext.createSSLEngine();
tlsEngine.setUseClientMode(clientMode);
SSLSession sslSession = tlsEngine.getSession();
final SSLSession sslSession = sslEngine.getSession();
netBuffSize = sslSession.getPacketBufferSize();
appBuffSize = sslSession.getApplicationBufferSize();
}
catch ( NoSuchAlgorithmException | KeyManagementException ex )
catch ( NoSuchAlgorithmException | KeyManagementException | KeyStoreException ex )
{
Log.error("TLSHandler startup problem. SSLContext initialisation failed.", ex );
}
......
......@@ -160,6 +160,7 @@ public abstract class VirtualConnection implements Connection {
}
@Override
public void addCompression() {
//Ignore
}
......
......@@ -28,6 +28,7 @@ import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.UnknownHostException;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
......@@ -46,6 +47,7 @@ import org.apache.mina.core.filterchain.IoFilterChain;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.compression.CompressionFilter;
import org.apache.mina.filter.ssl.SslFilter;
import org.dom4j.io.OutputFormat;
import org.jivesoftware.openfire.Connection;
import org.jivesoftware.openfire.ConnectionCloseListener;
import org.jivesoftware.openfire.PacketDeliverer;
......@@ -58,6 +60,7 @@ import org.jivesoftware.openfire.session.ConnectionSettings;
import org.jivesoftware.openfire.session.LocalSession;
import org.jivesoftware.openfire.session.Session;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.XMLWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.packet.Packet;
......@@ -74,6 +77,12 @@ public class NIOConnection implements Connection {
private static final Logger Log = LoggerFactory.getLogger(NIOConnection.class);
public enum State { RUNNING, CLOSING, CLOSED }
/**
* The utf-8 charset for decoding and encoding XMPP packet streams.
*/
public static final String CHARSET = "UTF-8";
private LocalSession session;
private IoSession ioSession;
......@@ -125,6 +134,7 @@ public class NIOConnection implements Connection {
public NIOConnection(IoSession session, PacketDeliverer packetDeliverer) {
this.ioSession = session;
this.backupDeliverer = packetDeliverer;
state = State.RUNNING;
}
@Override
......@@ -368,7 +378,7 @@ public class NIOConnection implements Connection {
@Deprecated
@Override
public void startTLS(boolean clientMode, String remoteServer, ClientAuth authentication) throws Exception {
final boolean isClientToServer = (remoteServer == null);
final boolean isClientToServer = ( remoteServer == null );
startTLS( clientMode, isClientToServer, authentication );
}
......
......@@ -427,7 +427,6 @@ public class ConnectionManagerImpl extends BasicModule implements ConnectionMana
// Start clients SSL unless it's been disabled.
if (isClientSSLListenerEnabled()) {
int port = getClientSSLListenerPort();
String algorithm = JiveGlobals.getProperty(ConnectionSettings.Client.TLS_ALGORITHM, "TLS");
try {
// Customize Executor that will be used by processors to process incoming stanzas
int maxPoolSize = JiveGlobals.getIntProperty(ConnectionSettings.Client.MAX_THREADS_SSL, 16);
......@@ -453,19 +452,14 @@ public class ConnectionManagerImpl extends BasicModule implements ConnectionMana
sslSocketAcceptor, maxBufferSize);
// Add the SSL filter now since sockets are "borned" encrypted in the old ssl method
final IdentityStoreConfig identityStoreConfig = (IdentityStoreConfig) SSLConfig.getInstance().getStoreConfig( Purpose.SOCKETBASED_IDENTITYSTORE );
final TrustStoreConfig trustStoreConfig = (TrustStoreConfig) SSLConfig.getInstance().getStoreConfig( Purpose.SOCKETBASED_C2S_TRUSTSTORE );
final SSLContext sslContext = SSLConfig.getSSLContext();
sslContext.init( identityStoreConfig.getKeyManagers(), trustStoreConfig.getTrustManagers(), new java.security.SecureRandom());
SslFilter sslFilter = new SslFilter(sslContext);
if (JiveGlobals.getProperty(ConnectionSettings.Client.AUTH_PER_CLIENTCERT_POLICY,"disabled").equals("needed")) {
sslFilter.setNeedClientAuth(true);
}
else if(JiveGlobals.getProperty(ConnectionSettings.Client.AUTH_PER_CLIENTCERT_POLICY,"disabled").equals("wanted")) {
sslFilter.setWantClientAuth(true);
Connection.ClientAuth clientAuth;
try {
clientAuth = Connection.ClientAuth.valueOf(JiveGlobals.getProperty(ConnectionSettings.Client.AUTH_PER_CLIENTCERT_POLICY, "disabled"));
} catch (IllegalArgumentException e) {
clientAuth = Connection.ClientAuth.disabled;
}
final SslFilter sslFilter = SSLConfig.getServerModeSslFilter( SSLConfig.Type.SOCKET_C2S, clientAuth );
sslSocketAcceptor.getFilterChain().addAfter(EXECUTOR_FILTER_NAME, TLS_FILTER_NAME, sslFilter);
}
......@@ -496,7 +490,7 @@ public class ConnectionManagerImpl extends BasicModule implements ConnectionMana
ports.add(new ServerPort(port, serverName, localIPAddress, true, null, ServerPort.Type.client));
List<String> params = new ArrayList<>();
List<String> params = new ArrayList<String>();
params.add(Integer.toString(port));
Log.info(LocaleUtils.getLocalizedString("startup.ssl", params));
}
......
......@@ -63,6 +63,7 @@ import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;
import org.bouncycastle.x509.X509V3CertificateGenerator;
import org.jivesoftware.openfire.keystore.CertificateStoreConfig;
import org.jivesoftware.openfire.keystore.CertificateStoreConfigException;
import org.jivesoftware.openfire.keystore.CertificateUtils;
import org.jivesoftware.util.cert.CertificateIdentityMapping;
import org.jivesoftware.util.cert.CNCertificateIdentityMapping;
import org.jivesoftware.util.cert.SANCertificateIdentityMapping;
......@@ -851,69 +852,12 @@ public class CertificateManager {
*
* @param certificates an unordered collection of certificates (cannot be null).
* @return An ordered list of certificates (possibly empty, but never null).
* @deprecated Moved to CertificateUtils
*/
@Deprecated
public static List<X509Certificate> order( Collection<X509Certificate> certificates ) throws CertificateException
{
final LinkedList<X509Certificate> orderedResult = new LinkedList<>();
if ( certificates.isEmpty() ) {
return orderedResult;
}
if (certificates.size() == 1) {
orderedResult.addAll( certificates );
return orderedResult;
}
final Map<Principal, X509Certificate> byIssuer = new HashMap<>();
final Map<Principal, X509Certificate> bySubject = new HashMap<>();
for ( final X509Certificate certificate : certificates ) {
final Principal issuer = certificate.getIssuerDN();
final Principal subject = certificate.getSubjectDN();
if ( byIssuer.put( issuer, certificate ) != null ) {
throw new CertificateException( "The provided input should not contain multiple certificates with identical issuerDN values." );
}
if ( bySubject.put( subject, certificate ) != null ) {
throw new CertificateException( "The provided input should not contain multiple certificates with identical subjectDN values." );
}
}
// The first certificate will have a 'subject' value that's not an 'issuer' of any other chain.
X509Certificate first = null;
for ( Map.Entry<Principal, X509Certificate> entry : bySubject.entrySet() ) {
final Principal subject = entry.getKey();
final X509Certificate certificate = entry.getValue();
if ( ! byIssuer.containsKey( subject ) ) {
if (first == null) {
first = certificate;
} else {
throw new CertificateException( "The provided input should not contain more than one certificates that has a subjectDN value that's not equal to the issuerDN value of another certificate." );
}
}
}
if (first == null) {
throw new CertificateException( "The provided input should contain a certificates that has a subjectDN value that's not equal to the issuerDN value of any other certificate." );
}
orderedResult.add( first );
// With the first certificate in hand, every following certificate should have a subject that's equal to the previous issuer value.
X509Certificate next = bySubject.get( first.getIssuerDN() );
while (next != null) {
orderedResult.add( next );
next = bySubject.get( next.getIssuerDN() );
}
// final check
if (orderedResult.size() != certificates.size()) {
throw new CertificateException( "Unable to recreate a certificate chain from the provided input." );
}
return orderedResult;
return CertificateUtils.order( certificates );
}
/**
......
......@@ -58,7 +58,7 @@ public class SimpleSSLSocketFactory extends SSLSocketFactory implements Comparat
public SimpleSSLSocketFactory() {
try {
final SSLContext sslContext = SSLConfig.getSSLContext();
final SSLContext sslContext = SSLContext.getDefault();
sslContext.init(null, // KeyManager not required
new TrustManager[] { new DummyTrustManager() },
new java.security.SecureRandom());
......
package org.jivesoftware.openfire.keystore;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.Assert;
import org.junit.Test;
import java.security.Provider;
import java.security.Security;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
/**
* Unit tests that verify the functionality of {@link CertificateUtils}.
*
* @author Guus der Kinderen, guus.der.kinderen@gmail.com
*/
public class CertificateUtilsTest
{
/**
* Test for {@link CertificateUtils#filterValid(Collection)}. Verifies that an input argument that
* is null returns an empty collection.
*/
@Test
public void testFilterValidNull() throws Exception
{
// Setup fixture.
final Collection<X509Certificate> input = null;
// Execute system under test.
final Collection<X509Certificate> result = CertificateUtils.filterValid( input );
// Verify results.
Assert.assertTrue( result.isEmpty() );
}
/**
* Test for {@link CertificateUtils#filterValid(Collection)}. Verifies that an input argument that
* is an empty (not-null) collection returns an empty collection.
*/
@Test
public void testFilterValidEmpty() throws Exception
{
// Setup fixture.
final Collection<X509Certificate> input = new ArrayList<>();
// Execute system under test.
final Collection<X509Certificate> result = CertificateUtils.filterValid( input );
// Verify results.
Assert.assertTrue( result.isEmpty() );
}
/**
* Test for {@link CertificateUtils#filterValid(Collection)}. Verifies that an input argument that
* contains one valid certificate returns an collection that contains that certificate.
*/
@Test
public void testFilterValidWithOneValidCert() throws Exception
{
// Setup fixture.
final X509Certificate valid = KeystoreTestUtils.generateValidCertificate();
final Collection<X509Certificate> input = new ArrayList<>();
input.add( valid );
// Execute system under test.
final Collection<X509Certificate> result = CertificateUtils.filterValid( input );
// Verify results.
Assert.assertEquals( 1, result.size() );
Assert.assertTrue( result.contains( valid ) );
}
/**
* Test for {@link CertificateUtils#filterValid(Collection)}. Verifies that an input argument that
* contains one invalid certificate returns an collection that is empty.
*/
@Test
public void testFilterValidWithOneInvalidCert() throws Exception
{
// Setup fixture.
final X509Certificate invalid = KeystoreTestUtils.generateExpiredCertificate();
final Collection<X509Certificate> input = new ArrayList<>();
input.add( invalid );
// Execute system under test.
final Collection<X509Certificate> result = CertificateUtils.filterValid( input );
// Verify results.
Assert.assertTrue( result.isEmpty() );
}
/**
* Test for {@link CertificateUtils#filterValid(Collection)}. Verifies that an input argument that
* contains two duplicate, valid certificates returns an collection that contains that certificate once.
*/
@Test
public void testFilterValidWithTwoDuplicateValidCerts() throws Exception
{
// Setup fixture.
final X509Certificate valid = KeystoreTestUtils.generateValidCertificate();
final Collection<X509Certificate> input = new ArrayList<>();
input.add( valid );
input.add( valid );
// Execute system under test.
final Collection<X509Certificate> result = CertificateUtils.filterValid( input );
// Verify results.
Assert.assertEquals( 1, result.size() );
Assert.assertTrue( result.contains( valid ) );
}
/**
* Test for {@link CertificateUtils#filterValid(Collection)}. Verifies that an input argument that
* contains two distinct, valid certificates returns an collection that contains both certificates.
*/
@Test
public void testFilterValidWithTwoDistinctValidCerts() throws Exception
{
// Setup fixture.
final X509Certificate validA = KeystoreTestUtils.generateValidCertificate();
final X509Certificate validB = KeystoreTestUtils.generateValidCertificate();
final Collection<X509Certificate> input = new ArrayList<>();
input.add( validA );
input.add( validB );
// Execute system under test.
final Collection<X509Certificate> result = CertificateUtils.filterValid( input );
// Verify results.
Assert.assertEquals( 2, result.size() );
Assert.assertTrue( result.contains( validA ) );
Assert.assertTrue( result.contains( validB ) );
}
/**
* Test for {@link CertificateUtils#filterValid(Collection)}. Verifies that an input argument that
* contains two duplicate, invalid certificate returns an collection that is empty.
*/
@Test
public void testFilterValidWithTwoDuplicateInvalidCerts() throws Exception
{
// Setup fixture.
final X509Certificate invalid = KeystoreTestUtils.generateExpiredCertificate();
final Collection<X509Certificate> input = new ArrayList<>();
input.add( invalid );
input.add( invalid );
// Execute system under test.
final Collection<X509Certificate> result = CertificateUtils.filterValid( input );
// Verify results.
Assert.assertTrue( result.isEmpty() );
}
/**
* Test for {@link CertificateUtils#filterValid(Collection)}. Verifies that an input argument that
* contains two distinct, invalid certificate returns an collection that is empty.
*/
@Test
public void testFilterValidWithTwoDistinctInvalidCerts() throws Exception
{
// Setup fixture.
final X509Certificate invalidA = KeystoreTestUtils.generateExpiredCertificate();
final X509Certificate invalidB = KeystoreTestUtils.generateExpiredCertificate();
final Collection<X509Certificate> input = new ArrayList<>();
input.add( invalidA );
input.add( invalidB );
// Execute system under test.
final Collection<X509Certificate> result = CertificateUtils.filterValid( input );
// Verify results.
Assert.assertTrue( result.isEmpty() );
}
/**
* Test for {@link CertificateUtils#filterValid(Collection)}. Verifies that an input argument that
* contains one valid and one invalid valid certificatereturns an collection that contains one valid certificate.
*/
@Test
public void testFilterValidWithValidAndInvalidCerts() throws Exception
{
// Setup fixture.
final X509Certificate valid = KeystoreTestUtils.generateValidCertificate();
final X509Certificate invalid = KeystoreTestUtils.generateExpiredCertificate();
final Collection<X509Certificate> input = new ArrayList<>();
input.add( valid );
input.add( invalid );
// Execute system under test.
final Collection<X509Certificate> result = CertificateUtils.filterValid( input );
// Verify results.
Assert.assertEquals( 1, result.size() );
Assert.assertTrue( result.contains( valid ) );
}
/**
* Test for {@link CertificateUtils#filterValid(Collection)}. Verifies that an input argument that
* contains:
* - one valid
* - another valid (duplicated from the first)
* - a third valid (no duplicate)
* - and one invalid valid certificate
* returns an collection that contains the two distinc valid certificates.
*/
@Test
public void testFilterValidWithMixOfValidityAndDuplicates() throws Exception
{
// Setup fixture.
final X509Certificate validA = KeystoreTestUtils.generateValidCertificate();
final X509Certificate validB = KeystoreTestUtils.generateValidCertificate();
final X509Certificate invalid = KeystoreTestUtils.generateExpiredCertificate();
final Collection<X509Certificate> input = new ArrayList<>();
input.add( validA );
input.add( validA );
input.add( validB );
input.add( invalid );
// Execute system under test.
final Collection<X509Certificate> result = CertificateUtils.filterValid( input );
// Verify results.
Assert.assertEquals( 2, result.size() );
Assert.assertTrue( result.contains( validA ) );
Assert.assertTrue( result.contains( validB ) );
}
}
package org.jivesoftware.openfire.keystore;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
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 OpenfireX509ExtendedTrustManager#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 = "acceptSelfSigned={0},checkValidity={1}" )
public static Iterable<Object[]> constructorArguments()
{
final List<Object[]> constructorArguments = new ArrayList<>();
constructorArguments.add( new Object[] { false, true } ); // acceptSelfSigned = false, check validity = true
constructorArguments.add( new Object[] { false, false } ); // acceptSelfSigned = false, check validity = false
constructorArguments.add( new Object[] { true, true } ); // acceptSelfSigned = true, check validity = true
constructorArguments.add( new Object[] { true, false } ); // acceptSelfSigned = 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 OpenfireX509ExtendedTrustManager 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 OpenfireX509ExtendedTrustManager( 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 );
}
}
}
package org.jivesoftware.openfire.keystore;
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;
import sun.security.provider.X509Factory;
import java.math.BigInteger;
import java.security.*;
import java.security.cert.*;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
/**
* 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();
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( X509Factory.BEGIN_CERT ).append( '\n' );
sb.append( Base64.encodeBytes( certificate.getEncoded() ) ).append( '\n' );
sb.append( X509Factory.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( 1024 );
// 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( 1024 );
// 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( 1024 );
// 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=MyName" + subjectKeyPair.getPublic().hashCode() );
final X500Name issuer = new X500Name( "CN=MyName" + issuerKeyPair.getPublic().hashCode() );
// 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( 1024 );
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 );
}
}
package org.jivesoftware.openfire.keystore;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import java.io.File;
import java.io.FileOutputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.KeyStore;
import java.security.cert.*;
import java.util.*;
/**
* Unit tests that verify the functionality of {@link OpenfireX509ExtendedTrustManager}.
*
* @author Guus der Kinderen, guus.der.kinderen@gmail.com
*/
public class OpenfireX509ExtendedTrustManagerTest
{
/**
* An instance that is freshly recreated before each test.
*/
private OpenfireX509ExtendedTrustManager systemUnderTest;
/**
* The keystore that contains the certificates used by the system under test (refreshed before every test invocation).
*/
private KeyStore trustStore;
private String location; // location on disc for the keystore. Used to clean-up after each test.
private X509Certificate[] validChain;
private X509Certificate[] expiredIntChain;
private X509Certificate[] expiredRootChain;
private X509Certificate[] untrustedCAChain;
@Before
public void createFixture() throws Exception
{
// Create a fresh store in a location that holds only temporary files.
final String tempDir = System.getProperty("java.io.tmpdir");
location = tempDir + ( tempDir.endsWith( File.separator ) ? "" : File.separator ) + UUID.randomUUID();
trustStore = KeyStore.getInstance( KeyStore.getDefaultType());
final String password = "TS%WV@# aSG 4";
trustStore.load( null, password.toCharArray() );
// Populate the store with a valid CA certificate.
validChain = KeystoreTestUtils.generateValidCertificateChain();
expiredIntChain = KeystoreTestUtils.generateCertificateChainWithExpiredIntermediateCert();
expiredRootChain = KeystoreTestUtils.generateCertificateChainWithExpiredRootCert();
untrustedCAChain = KeystoreTestUtils.generateValidCertificateChain();
trustStore.setCertificateEntry( getLast( validChain ).getSubjectDN().getName(), getLast( validChain ) );
trustStore.setCertificateEntry( getLast( expiredIntChain ).getSubjectDN().getName(), getLast( expiredIntChain ) );
trustStore.setCertificateEntry( getLast( expiredRootChain ).getSubjectDN().getName(), getLast( expiredRootChain ) );
// Persist the key store file
try ( FileOutputStream fos = new FileOutputStream( location ) ) {
trustStore.store( fos, password.toCharArray() );
}
// Create the Trust Manager that is subject of these tests.
systemUnderTest = null; // TODO FIXME // new OpenfireX509ExtendedTrustManager( trustStore );
}
/**
* 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 extends Object> X getLast(X[]chain) {
return chain[ chain.length - 1 ];
}
@After
public void tearDown() throws Exception
{
// Attempt to delete any left-overs from the test.
validChain = null;
if ( trustStore != null)
{
trustStore = null;
Files.deleteIfExists( Paths.get( location ) );
}
systemUnderTest = null;
}
/**
* This test verifies that {@link OpenfireX509ExtendedTrustManager#getAcceptedIssuers()} does not return expired
* certificates.
*/
@Test
public void testAcceptedIssuersAreAllValid() throws Exception
{
// Setup fixture.
// Execute system under test.
final X509Certificate[] result = systemUnderTest.getAcceptedIssuers();
// Verify results.
Assert.assertEquals( 2, result.length );
Assert.assertTrue( Arrays.asList( result ).contains( getLast( validChain ) ) );
Assert.assertTrue( Arrays.asList( result ).contains( getLast( expiredIntChain ) ) );
Assert.assertFalse( Arrays.asList( result ).contains( getLast( expiredRootChain ) ) );
}
/**
* Verifies that a valid chain is not rejected.
*/
@Test
public void testValidChain() throws Exception
{
// Setup fixture.
// Execute system under test.
systemUnderTest.checkClientTrusted( validChain, "RSA" );
// Verify result
// (getting here without an exception being thrown is enough).
}
/**
* Verifies that a chain that has an intermediate certificate that is expired is rejected.
*/
@Test(expected = CertificateException.class)
public void testInvalidChainExpiredIntermediate() throws Exception
{
// Setup fixture.
// Execute system under test.
systemUnderTest.checkClientTrusted( expiredIntChain, "RSA" );
}
/**
* Verifies that a chain that has an root certificate (trust anchor) that is expired is rejected.
*/
@Test(expected = CertificateException.class)
public void testInvalidChainExpiredTrustAnchor() throws Exception
{
// Setup fixture.
// Execute system under test.
systemUnderTest.checkClientTrusted( expiredRootChain, "RSA" );
}
/**
* Verifies that a chain that is missing an intermediate certificate is rejected.
*/
@Test(expected = CertificateException.class)
public void testInvalidChainMissingIntermediate() throws Exception
{
// Setup fixture.
assert validChain.length == 4;
final X509Certificate[] input = new X509Certificate[ 3 ];
input[ 0 ] = validChain[ 0 ];
input[ 1 ] = validChain[ 2 ];
input[ 2 ] = validChain[ 3 ];
// Execute system under test.
systemUnderTest.checkClientTrusted( input, "RSA" );
}
/**
* Verifies that a chain that is valid, but does not have its root CA certificate in the trust store, is rejected.
*/
@Test(expected = CertificateException.class)
public void testInvalidChainCAnotTrusted() throws Exception
{
// Setup fixture.
// Execute system under test.
systemUnderTest.checkClientTrusted( untrustedCAChain, "RSA" );
}
}
package org.jivesoftware.openfire.keystore;
import java.io.File;
import java.io.FileOutputStream;
import java.math.BigInteger;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.Provider;
import java.security.SecureRandom;
import java.security.Security;
import java.security.cert.TrustAnchor;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import org.bouncycastle.jce.X509Principal;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.x509.X509V3CertificateGenerator;
......@@ -28,6 +8,17 @@ import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import sun.security.provider.X509Factory;
import java.io.File;
import java.io.FileOutputStream;
import java.math.BigInteger;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.*;
import java.security.cert.TrustAnchor;
import java.security.cert.X509Certificate;
import java.util.*;
/**
* Unit tests that verify the functionality of {@link TrustStoreConfig}.
......@@ -36,181 +27,139 @@ import org.junit.Test;
*/
public class TrustStoreConfigTest
{
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 );
}
/**
* An instance that is freshly recreated before each test.
*/
private TrustStoreConfig trustStoreConfig;
@Before
public void createFixture() throws Exception
{
// Create a fresh store in a location that holds only temporary files.
final String tempDir = System.getProperty("java.io.tmpdir");
final String location = tempDir + ( tempDir.endsWith( File.separator ) ? "" : File.separator ) + UUID.randomUUID();
final KeyStore keyStore = KeyStore.getInstance( KeyStore.getDefaultType());
final String password = "TS%WV@# aSG 4";
keyStore.load( null, password.toCharArray() );
// Populate the store with a valid CA certificate.
final X509Certificate validCertificate = generateTestSelfSignedCertificate( true );
keyStore.setCertificateEntry( "valid-ca", validCertificate );
// Populate the store with an invalid CA certificate.
final X509Certificate invalidCertificate = generateTestSelfSignedCertificate( false );
keyStore.setCertificateEntry( "invalid-ca", invalidCertificate );
// Persist the keystore file
try ( FileOutputStream fos = new FileOutputStream( location ) ) {
keyStore.store( fos, password.toCharArray() );
}
// Use the new keystore file to create a fresh trust store, which will be used as a fixture by the tests.
trustStoreConfig = new TrustStoreConfig( location, password, keyStore.getType(), false );
}
private static X509Certificate generateTestSelfSignedCertificate( boolean isValid ) throws Exception
{
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance( "RSA" );
keyPairGenerator.initialize( 1024 );
KeyPair KPair = keyPairGenerator.generateKeyPair();
X509V3CertificateGenerator v3CertGen = new X509V3CertificateGenerator();
v3CertGen.setSerialNumber( BigInteger.valueOf( Math.abs( new SecureRandom().nextInt() ) ) );
X509Principal principal;
if ( isValid ) {
principal = new X509Principal("CN=valid.example.org, OU=None, O=None L=None, C=None");
v3CertGen.setNotBefore( new Date( System.currentTimeMillis() - 1000L * 60 * 60 * 24 * 30 ) );
v3CertGen.setNotAfter( new Date( System.currentTimeMillis() + ( 1000L * 60 * 60 * 24 * 365 * 10 ) ) );
} else {
principal = new X509Principal("CN=invalid.example.org, OU=None, O=None L=None, C=None");
v3CertGen.setNotBefore( new Date( System.currentTimeMillis() - 1000L * 60 * 60 * 24 * 30 ) );
v3CertGen.setNotAfter( new Date( System.currentTimeMillis() - 1000L * 60 * 60 * 24 * 30 ) );
}
v3CertGen.setIssuerDN( principal );
v3CertGen.setSubjectDN( principal );
v3CertGen.setPublicKey( KPair.getPublic() );
v3CertGen.setSignatureAlgorithm( "MD5WithRSAEncryption" );
return v3CertGen.generateX509Certificate(KPair.getPrivate());
}
private static String toPEM( 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();
}
@After
public void tearDown() throws Exception
{
// Attempt to delete any left-overs from the test.
if (trustStoreConfig != null)
{
Files.deleteIfExists( Paths.get( trustStoreConfig.getCanonicalPath() ) );
trustStoreConfig = null;
}
}
/**
* The store in the fixture contains two certificates - one that is valid, and one that is invalid.
*
* This test verifies that {@link TrustStoreConfig#getAllCertificates()} returns both.
*/
@Test
public void testGetAll() throws Exception
{
// Setup fixture.
// Execute system under test.
final Map<String, X509Certificate> result = trustStoreConfig.getAllCertificates();
// Verify results.
Assert.assertEquals( 2, result.size() );
}
/**
* The store in the fixture contains two certificates - one that is valid, and one that is invalid.
*
* This test verifies that {@link TrustStoreConfig#getAllValidTrustAnchors()} returns only the valid one.
*/
@Test
public void testGetValid() throws Exception
{
// Setup fixture.
// Execute system under test.
final Set<TrustAnchor> result = trustStoreConfig.getAllValidTrustAnchors();
// Verify results.
Assert.assertEquals( 1, result.size() );
Assert.assertTrue( result.iterator().next().getTrustedCert().getIssuerDN().getName().contains( "CN=valid.example.org" ) );
}
/**
* A chain that has a trust anchor in the trust store (and is otherwise valid) should be trusted.
*/
@Test
public void testTrustCertSignedByCA() throws Exception
{
// Setup fixture
final Collection<X509Certificate> chain = new HashSet<>();
chain.add( (X509Certificate) trustStoreConfig.getStore().getCertificate( "valid-ca" ) ); // somewhat of a hack. Should use a distinct cert for the test.
// Execute System Under Test
final boolean result = trustStoreConfig.canTrust( chain );
// Verify
Assert.assertTrue( result );
}
/**
* A chain that has no trust anchor in the trust store (but is otherwise valid) should not be trusted.
*/
@Test
public void testDontTrustCertNotSignedByCA() throws Exception
{
// Setup fixture
final Collection<X509Certificate> chain = new HashSet<>();
chain.add( generateTestSelfSignedCertificate( true ) );
// Execute System Under Test
final boolean result = trustStoreConfig.canTrust( chain );
// Verify
Assert.assertFalse( result );
}
/**
* This test verifies that when a certificate is installed in the store using
* {@link TrustStoreConfig#installCertificate(String, String)} a certificate chain of which the anchor is that same
* certificate is successfully verified.
*/
@Test
public void verifyWithNewlyInstalledCACert() throws Exception
{
// Setup fixture
final X509Certificate cert = generateTestSelfSignedCertificate( true );
final String pemCert = toPEM( cert );
final Collection<X509Certificate> chain = new HashSet<>();
chain.add( cert ); // somewhat of a hack. Should use a distinct cert for the test.
// Execute System Under Test
trustStoreConfig.installCertificate( "new-cert", pemCert );
final boolean result = trustStoreConfig.canTrust( chain );
// Verify
Assert.assertTrue( result );
}
// /**
// * An instance that is freshly recreated before each test.
// */
// private TrustStoreConfig trustStoreConfig;
//
// @Before
// public void createFixture() throws Exception
// {
// // Create a fresh store in a location that holds only temporary files.
// final String tempDir = System.getProperty("java.io.tmpdir");
// final String location = tempDir + ( tempDir.endsWith( File.separator ) ? "" : File.separator ) + UUID.randomUUID();
//
// final KeyStore keyStore = KeyStore.getInstance( KeyStore.getDefaultType());
// final String password = "TS%WV@# aSG 4";
// keyStore.load( null, password.toCharArray() );
//
// // Populate the store with a valid CA certificate.
// final X509Certificate validCertificate = KeystoreTestUtils.generateValidCertificate();
// keyStore.setCertificateEntry( "valid-ca", validCertificate );
//
// // Populate the store with an invalid CA certificate. This certificate is invalid as its validity period has expired.
// final X509Certificate invalidCertificate = KeystoreTestUtils.generateExpiredCertificate();
// keyStore.setCertificateEntry( "invalid-ca", invalidCertificate );
//
// // Persist the keystore file
// try ( FileOutputStream fos = new FileOutputStream( location ) ) {
// keyStore.store( fos, password.toCharArray() );
// }
//
// // Use the new keystore file to create a fresh trust store, which will be used as a fixture by the tests.
// trustStoreConfig = new TrustStoreConfig( location, password, keyStore.getType(), false );
// }
//
// @After
// public void tearDown() throws Exception
// {
// // Attempt to delete any left-overs from the test.
// if (trustStoreConfig != null)
// {
// Files.deleteIfExists( Paths.get( trustStoreConfig.getCanonicalPath() ) );
// trustStoreConfig = null;
// }
// }
//
// /**
// * The store in the fixture contains two certificates - one that is valid, and one that is invalid.
// *
// * This test verifies that {@link TrustStoreConfig#getAllCertificates()} returns both.
// */
// @Test
// public void testGetAll() throws Exception
// {
// // Setup fixture.
//
// // Execute system under test.
// final Map<String, X509Certificate> result = trustStoreConfig.getAllCertificates();
//
// // Verify results.
// Assert.assertEquals( 2, result.size() );
// }
// /**
// * The store in the fixture contains two certificates - one that is valid, and one that is invalid.
// *
// * This test verifies that {@link TrustStoreConfig#getAllValidTrustAnchors()} returns only the valid one.
// */
// @Test
// public void testGetValid() throws Exception
// {
// // Setup fixture.
//
// // Execute system under test.
// final Set<TrustAnchor> result = trustStoreConfig.getAllValidTrustAnchors();
//
// // Verify results.
// Assert.assertEquals( 1, result.size() );
// Assert.assertTrue( result.iterator().next().getTrustedCert().getIssuerDN().getName().contains( "CN=valid.example.org" ) );
// }
//
// /**
// * A chain that has a trust anchor in the trust store (and is otherwise valid) should be trusted.
// */
// @Test
// public void testTrustCertSignedByCA() throws Exception
// {
// // Setup fixture
// final Collection<X509Certificate> chain = new HashSet<>();
// chain.add( (X509Certificate) trustStoreConfig.getStore().getCertificate( "valid-ca" ) ); // somewhat of a hack. Should use a distinct cert for the test.
//
// // Execute System Under Test
// final boolean result = trustStoreConfig.canTrust( chain );
//
// // Verify
// Assert.assertTrue( result );
// }
//
// /**
// * A chain that has no trust anchor in the trust store (but is otherwise valid) should not be trusted.
// */
// @Test
// public void testDontTrustCertNotSignedByCA() throws Exception
// {
// // Setup fixture
// final Collection<X509Certificate> chain = new HashSet<>();
// chain.add( KeystoreTestUtils.generateValidCertificate() );
//
// // Execute System Under Test
// final boolean result = trustStoreConfig.canTrust( chain );
//
// // Verify
// Assert.assertFalse( result );
// }
//
// /**
// * This test verifies that when a certificate is installed in the store using
// * {@link TrustStoreConfig#installCertificate(String, String)} a certificate chain of which the anchor is that same
// * certificate is successfully verified.
// */
// @Test
// public void verifyWithNewlyInstalledCACert() throws Exception
// {
// // Setup fixture
// final X509Certificate cert = KeystoreTestUtils.generateValidCertificate();
// final String pemCert = KeystoreTestUtils.toPemFormat( cert );
//
// final Collection<X509Certificate> chain = new HashSet<>();
// chain.add( cert ); // somewhat of a hack. Should use a distinct cert for the test.
//
// // Execute System Under Test
// trustStoreConfig.installCertificate( "new-cert", pemCert );
// final boolean result = trustStoreConfig.canTrust( chain );
//
// // Verify
// Assert.assertTrue( result );
// }
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment