Commit 2f5191f0 authored by Guus der Kinderen's avatar Guus der Kinderen

OF-1373: Allow certificate changes to be applied without a restart.

This commit grew a little larger than anticipated - my apologies for the reviewer. The original intend
for this was to allow changes in the java Keystore to be applied, without requiring a restart of Openfire.
This is what allows outside processes to update the Keystores - think Let's Encrypt - but could also be
useful for other purposes.

In the process of writing this code, I've found that a restart of the Connection Acceptor would still cause
all existing connections to be terminated. This is why a 'reload configuration' method was added.

I've found that the old Certificate Event Listener methods were unuseful. They were pretty detailled, but
all implementations used them in the same way: "reload everything". I've replaced those with one
'something changed' event. Also, the event listeners are no longer triggered in various places in the code.
Instead, the event listeners will now be triggered by the filesystem-based change of the keystore - the same
event that's used to reload configuration when Let's Encrypt updates pop up.

I've removed various bits of lengthy, unused code in the old CertificateManager (primarily code that
interacts directly with KeyStores, as that caused timing issues during the reload).

One functional change (that was marked as 'unsure why we do this' in code) is that after this commit, the
content of the identity store is no longer merged with the content of the trust store, while determining if
a particular end-entity certificate is to be trusted.
parent 539cbf6a
......@@ -44,6 +44,7 @@ import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.eclipse.jetty.webapp.WebAppContext;
import org.jivesoftware.openfire.JMXManager;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.keystore.CertificateStore;
import org.jivesoftware.openfire.keystore.IdentityStore;
import org.jivesoftware.openfire.spi.ConnectionConfiguration;
import org.jivesoftware.openfire.spi.ConnectionManagerImpl;
......@@ -413,25 +414,9 @@ public class AdminConsolePlugin implements Plugin {
private class CertificateListener implements CertificateEventListener {
@Override
public void certificateCreated(KeyStore keyStore, String alias, X509Certificate cert) {
// If new certificate is RSA then (re)start the HTTPS service
if ("RSA".equals(cert.getPublicKey().getAlgorithm())) {
restartNeeded = true;
}
}
@Override
public void certificateDeleted(KeyStore keyStore, String alias) {
restartNeeded = true;
}
@Override
public void certificateSigned(KeyStore keyStore, String alias,
List<X509Certificate> certificates) {
// If new certificate is RSA then (re)start the HTTPS service
if ("RSA".equals(certificates.get(0).getPublicKey().getAlgorithm())) {
public void storeContentChanged( CertificateStore store )
{
restartNeeded = true;
}
}
}
}
......@@ -35,6 +35,7 @@ import org.eclipse.jetty.webapp.WebAppContext;
import org.jivesoftware.openfire.Connection;
import org.jivesoftware.openfire.JMXManager;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.keystore.CertificateStore;
import org.jivesoftware.openfire.keystore.IdentityStore;
import org.jivesoftware.openfire.spi.ConnectionConfiguration;
import org.jivesoftware.openfire.spi.ConnectionManagerImpl;
......@@ -866,24 +867,8 @@ public final class HttpBindManager implements CertificateEventListener, Property
}
@Override
public void certificateCreated(KeyStore keyStore, String alias, X509Certificate cert) {
// If new certificate is RSA then (re)start the HTTPS service
if ("RSA".equals(cert.getPublicKey().getAlgorithm())) {
restartServer();
}
}
@Override
public void certificateDeleted(KeyStore keyStore, String alias) {
restartServer();
}
@Override
public void certificateSigned(KeyStore keyStore, String alias,
List<X509Certificate> certificates) {
// If new certificate is RSA then (re)start the HTTPS service
if ("RSA".equals(certificates.get(0).getPublicKey().getAlgorithm())) {
public void storeContentChanged( CertificateStore store )
{
restartServer();
}
}
}
package org.jivesoftware.openfire.keystore;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.jivesoftware.util.CertificateManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
......@@ -89,6 +90,7 @@ public abstract class CertificateStore
try ( final FileInputStream is = new FileInputStream( configuration.getFile() ) )
{
store.load( is, configuration.getPassword() );
CertificateManager.fireCertificateStoreChanged( this );
}
catch ( IOException | NoSuchAlgorithmException | CertificateException ex )
{
......@@ -143,7 +145,6 @@ public abstract class CertificateStore
* When the store does not contain an entry that matches the provided alias, this method does nothing.
*
* @param alias The alias for which to delete an entry (cannot be null or empty).
* @throws CertificateStoreConfigException
*/
public void delete( String alias ) throws CertificateStoreConfigException
{
......@@ -170,8 +171,6 @@ public abstract class CertificateStore
throw new CertificateStoreConfigException( "Unable to install a certificate into an identity store.", e );
}
// TODO: Notify listeners that a new certificate has been removed.
}
public KeyStore getStore()
......
......@@ -11,6 +11,7 @@ import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
......@@ -29,6 +30,8 @@ public class CertificateStoreManager extends BasicModule
private final ConcurrentMap<CertificateStoreConfiguration, IdentityStore> identityStores = new ConcurrentHashMap<>();
private final ConcurrentMap<CertificateStoreConfiguration, TrustStore> trustStores = new ConcurrentHashMap<>();
private CertificateStoreWatcher storeWatcher;
public CertificateStoreManager( )
{
super( "Certificate Store Manager" );
......@@ -39,7 +42,9 @@ public class CertificateStoreManager extends BasicModule
{
super.initialize( server );
for ( ConnectionType type : ConnectionType.values() )
storeWatcher = new CertificateStoreWatcher();
for ( final ConnectionType type : ConnectionType.values() )
{
try
{
......@@ -49,6 +54,7 @@ public class CertificateStoreManager extends BasicModule
{
final IdentityStore store = new IdentityStore( identityStoreConfiguration, false );
identityStores.put( identityStoreConfiguration, store );
storeWatcher.watch( store );
}
typeToIdentityStore.put( type, identityStoreConfiguration );
}
......@@ -65,6 +71,7 @@ public class CertificateStoreManager extends BasicModule
{
final TrustStore store = new TrustStore( trustStoreConfiguration, false );
trustStores.put( trustStoreConfiguration, store );
storeWatcher.watch( store );
}
typeToTrustStore.put( type, trustStoreConfiguration );
}
......@@ -78,6 +85,7 @@ public class CertificateStoreManager extends BasicModule
@Override
public synchronized void destroy()
{
storeWatcher.destroy();
typeToIdentityStore.clear();
typeToTrustStore.clear();
identityStores.clear();
......@@ -124,6 +132,7 @@ public class CertificateStoreManager extends BasicModule
// This constructor can throw an exception. If it does, the state of the manager should not have already changed.
final IdentityStore store = new IdentityStore( configuration, createIfAbsent );
identityStores.put( configuration, store );
storeWatcher.watch( store );
}
typeToIdentityStore.put( type, configuration );
......@@ -132,7 +141,11 @@ public class CertificateStoreManager extends BasicModule
// If the old store is not used by any other type, it can be shut down.
if ( oldConfig != null && !typeToIdentityStore.containsValue( oldConfig ) )
{
identityStores.remove( oldConfig );
final IdentityStore store = identityStores.remove( oldConfig );
if ( store != null )
{
storeWatcher.unwatch( store );
}
}
// Update all connection listeners that were using the old configuration.
......@@ -172,6 +185,7 @@ public class CertificateStoreManager extends BasicModule
// This constructor can throw an exception. If it does, the state of the manager should not have already changed.
final TrustStore store = new TrustStore( configuration, createIfAbsent );
trustStores.put( configuration, store );
storeWatcher.watch( store );
}
typeToTrustStore.put( type, configuration );
......@@ -180,7 +194,11 @@ public class CertificateStoreManager extends BasicModule
// If the old store is not used by any other type, it can be shut down.
if ( oldConfig != null && !typeToTrustStore.containsValue( oldConfig ) )
{
trustStores.remove( oldConfig );
final TrustStore store = trustStores.remove( oldConfig );
if ( store != null )
{
storeWatcher.unwatch( store );
}
}
// Update all connection listeners that were using the old configuration.
......
package org.jivesoftware.openfire.keystore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.file.*;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* Detects file-system based changes to (Java) keystores that back Openfire Certificate Stores, reloading them when
* needed.
*
* @author Guus der Kinderen, guus.der.kinderen@gmail.com
*/
public class CertificateStoreWatcher
{
private static final Logger Log = LoggerFactory.getLogger( CertificateStoreWatcher.class );
private final Map<CertificateStore, Path> watchedStores = new HashMap<>();
private final Map<Path, WatchKey> watchedPaths = new HashMap<>();
private WatchService storeWatcher;
private ExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
public CertificateStoreWatcher()
{
try
{
storeWatcher = FileSystems.getDefault().newWatchService();
executorService.submit( new Runnable()
{
@Override
public void run()
{
while ( !executorService.isShutdown() )
{
final WatchKey key;
try
{
key = storeWatcher.poll( 5, TimeUnit.SECONDS );
}
catch ( InterruptedException e )
{
// Interrupted. Stop waiting
continue;
}
if ( key == null )
{
continue;
}
for ( final WatchEvent<?> event : key.pollEvents() )
{
final WatchEvent.Kind<?> kind = event.kind();
// An OVERFLOW event can occur regardless of what kind of events the watcher was configured for.
if ( kind == StandardWatchEventKinds.OVERFLOW )
{
continue;
}
synchronized ( watchedStores )
{
// The filename is the context of the event.
final WatchEvent<Path> ev = (WatchEvent<Path>) event;
final Path changedFile = ((Path) key.watchable()).resolve( ev.context() );
// Can't use the value from the 'watchedStores' map, as that's the parent dir, not the keystore file!
for ( final CertificateStore store : watchedStores.keySet() )
{
final Path storeFile = store.getConfiguration().getFile().toPath().normalize();
if ( storeFile.equals( changedFile ) )
{
Log.info( "A file system change was detected. A(nother) certificate store that is backed by file '{}' will be reloaded.", storeFile );
try
{
store.reload();
}
catch ( CertificateStoreConfigException e )
{
Log.warn( "An unexpected exception occurred while trying to reload a certificate store that is backed by file '{}'!", storeFile, e );
}
}
}
}
}
// Reset the key to receive further events.
key.reset();
}
}
});
}
catch ( UnsupportedOperationException e )
{
storeWatcher = null;
Log.info( "This file system does not support watching file system objects for changes and events. Changes to Openfire certificate stores made outside of Openfire might not be detected. A restart of Openfire might be required for these to be applied." );
}
catch ( IOException e )
{
storeWatcher = null;
Log.warn( "An exception occured while trying to create a service that monitors the Openfire certificate stores for changes. Changes to Openfire certificate stores made outside of Openfire might not be detected. A restart of Openfire might be required for these to be applied.", e );
}
}
/**
* Shuts down this watcher, releasing all resources.
*/
public void destroy()
{
if ( executorService != null )
{
executorService.shutdown();
}
synchronized ( watchedStores )
{
if ( storeWatcher != null )
{
try
{
storeWatcher.close();
}
catch ( IOException e )
{
Log.warn( "Unable to close the watcherservice that is watching for file system changes to certificate stores.", e );
}
}
}
}
/**
* Start watching the file that backs a Certificate Store for changes, reloading the Certificate Store when
* appropriate.
*
* This method does nothing when the file watching functionality is not supported by the file system.
*
* @param store The certificate store (cannot be null).
*/
public void watch( CertificateStore store )
{
if ( store == null )
{
throw new IllegalArgumentException( "Argument 'store' cannot be null." );
}
if ( storeWatcher == null )
{
return;
}
final Path dir = store.getConfiguration().getFile().toPath().normalize().getParent();
synchronized ( watchedStores )
{
watchedStores.put( store, dir );
// Watch the directory that contains the keystore, if we're not already watching it.
if ( !watchedPaths.containsKey( dir ) )
{
try
{
// Ignoring deletion events, as those changes should be applied via property value changes.
final WatchKey watchKey = dir.register( storeWatcher, StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_CREATE );
watchedPaths.put( dir, watchKey );
}
catch ( Throwable t )
{
Log.warn( "Unable to add a watcher for a path that contains files that provides the backend storage for certificate stores. Changes to those files are unlikely to be picked up automatically. Path: {}", dir, t );
watchedStores.remove( store );
}
}
}
}
/**
* Stop watching the file that backs a Certificate Store for changes
*
* @param store The certificate store (cannot be null).
*/
public synchronized void unwatch( CertificateStore store )
{
if ( store == null )
{
throw new IllegalArgumentException( "Argument 'store' cannot be null." );
}
synchronized ( watchedStores )
{
watchedStores.remove( store );
final Path dir = store.getConfiguration().getFile().toPath().normalize().getParent();
// Check if there are any other stores being watched in the same directory.
if ( watchedStores.containsValue( dir ) )
{
return;
}
final WatchKey key = watchedPaths.remove( dir );
if ( key != null )
{
key.cancel();
}
}
}
}
......@@ -82,7 +82,140 @@ public class TrustStore extends CertificateStore
{
reload(); // re-initialize store.
}
}
/**
* Decide whether or not to trust the given supplied certificate chain. For certain failures, we SHOULD generate
* an exception - revocations and the like, but we currently do not.
*
* @param chain an array of X509Certificate where the first one is the endEntityCertificate.
* @return true if the content of this trust store allows the chain to be trusted, otherwise false.
*/
public boolean isTrusted( Certificate chain[] )
{
return getEndEntityCertificate( chain ) != null;
}
/**
* Decide whether or not to trust the given supplied certificate chain, returning the
* End Entity Certificate in this case where it can, and null otherwise.
* A self-signed certificate will, for example, return null.
* For certain failures, we SHOULD generate an exception - revocations and the like,
* but we currently do not.
*
* @param chain an array of X509Certificate where the first one is the endEntityCertificate.
* @return trusted end-entity certificate, or null.
*/
public X509Certificate getEndEntityCertificate( Certificate chain[] )
{
if ( chain == null || chain.length == 0 )
{
return null;
}
final X509Certificate first = (X509Certificate) chain[ 0 ];
try
{
first.checkValidity();
}
catch ( CertificateException e )
{
Log.warn( "EE Certificate not valid: " + e.getMessage() );
return null;
}
// TODO Notify listeners that a new certificate has been added.
if ( chain.length == 1 && first.getSubjectX500Principal().equals( first.getIssuerX500Principal() ) )
{
// Chain is single cert, and self-signed.
try
{
if ( store.getCertificateAlias( first ) != null )
{
// Interesting case: trusted self-signed cert.
return first;
}
}
catch ( KeyStoreException e )
{
Log.warn( "Keystore error while looking for self-signed cert; assuming untrusted." );
}
return null;
}
final List<Certificate> allCerts = new ArrayList<>();
try
{
// Add the trusted certs.
for ( Enumeration<String> aliases = store.aliases(); aliases.hasMoreElements(); )
{
String alias = aliases.nextElement();
if ( store.isCertificateEntry( alias ) )
{
X509Certificate cert = (X509Certificate) store.getCertificate( alias );
allCerts.add( cert );
}
}
// Finally, add all the certs in the chain:
allCerts.addAll( Arrays.asList( chain ) );
final CertStore cs = CertStore.getInstance( "Collection", new CollectionCertStoreParameters( allCerts ) );
final X509CertSelector selector = new X509CertSelector();
selector.setCertificate( first );
// / selector.setSubject(first.getSubjectX500Principal());
final PKIXBuilderParameters params = new PKIXBuilderParameters( store, selector );
params.addCertStore( cs );
params.setDate( new Date() );
params.setRevocationEnabled( false );
/* Code here is the right way to do things. */
final CertPathBuilder pathBuilder = CertPathBuilder.getInstance( CertPathBuilder.getDefaultType() );
final CertPath cp = pathBuilder.build( params ).getCertPath();
/*
* This section is an alternative to using CertPathBuilder which is not as complete (or safe), but will emit
* much better errors. If things break, swap around the code.
*
**** COMMENTED OUT. ****
*
final List<X509Certificate> ls = new ArrayList<>();
for ( final Certificate cert : chain )
{
ls.add( (X509Certificate) cert );
}
for ( X509Certificate last = ls.get( ls.size() - 1 );
!last.getIssuerX500Principal().equals( last.getSubjectX500Principal() );
last = ls.get( ls.size() - 1 ) )
{
final X509CertSelector sel = new X509CertSelector();
sel.setSubject( last.getIssuerX500Principal() );
ls.add( (X509Certificate) cs.getCertificates( sel ).toArray()[ 0 ] );
}
final CertPath cp = CertificateFactory.getInstance( "X.509" ).generateCertPath( ls );
****** END ALTERNATIVE. ****
*/
// Not entirely sure if I need to do this with CertPathBuilder. Can't hurt.
final CertPathValidator pathValidator = CertPathValidator.getInstance( "PKIX" );
pathValidator.validate( cp, params );
return (X509Certificate) cp.getCertificates().get( 0 );
}
catch ( CertPathBuilderException e )
{
Log.warn( "Path builder exception while validating certificate chain:", e );
}
catch ( CertPathValidatorException e )
{
Log.warn( "Path exception while validating certificate chain:", e );
}
catch ( Exception e )
{
Log.warn( "Unkown exception while validating certificate chain:" + e.getMessage() );
}
return null;
}
}
......@@ -26,6 +26,7 @@ import org.jivesoftware.openfire.XMPPServerInfo;
import org.jivesoftware.openfire.auth.AuthFactory;
import org.jivesoftware.openfire.auth.AuthToken;
import org.jivesoftware.openfire.keystore.CertificateStoreManager;
import org.jivesoftware.openfire.keystore.TrustStore;
import org.jivesoftware.openfire.lockout.LockOutManager;
import org.jivesoftware.openfire.sasl.Failure;
import org.jivesoftware.openfire.sasl.JiveSharedSecretSaslServer;
......@@ -40,7 +41,6 @@ import javax.security.sasl.Sasl;
import javax.security.sasl.SaslException;
import javax.security.sasl.SaslServer;
import javax.security.sasl.SaslServerFactory;
import java.security.KeyStore;
import java.security.Security;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
......@@ -194,16 +194,11 @@ public class SASLAuthentication {
if (mech.equals("EXTERNAL")) {
boolean trustedCert = false;
if (session.isSecure()) {
final LocalClientSession localClientSession = (LocalClientSession)session;
final Connection connection = localClientSession.getConnection();
final KeyStore keyStore = connection.getConfiguration().getIdentityStore().getStore();
final KeyStore trustStore = connection.getConfiguration().getTrustStore().getStore();
final X509Certificate trusted = CertificateManager.getEndEntityCertificate(connection.getPeerCertificates(), keyStore, trustStore);
if (trusted != null) {
trustedCert = true;
}
final Connection connection = ( (LocalClientSession) session ).getConnection();
final TrustStore trustStore = connection.getConfiguration().getTrustStore();
trustedCert = trustStore.isTrusted( connection.getPeerCertificates() );
}
if (trustedCert == false) {
if ( !trustedCert ) {
continue; // Do not offer EXTERNAL.
}
}
......@@ -218,9 +213,8 @@ public class SASLAuthentication {
final Element result = DocumentHelper.createElement( new QName( "mechanisms", new Namespace( "", SASL_NAMESPACE ) ) );
if (session.isSecure()) {
final Connection connection = session.getConnection();
final KeyStore keyStore = connection.getConfiguration().getIdentityStore().getStore();
final KeyStore trustStore = session.getConnection().getConfiguration().getTrustStore().getStore();
final X509Certificate trusted = CertificateManager.getEndEntityCertificate( session.getConnection().getPeerCertificates(), keyStore, trustStore );
final TrustStore trustStore = connection.getConfiguration().getTrustStore();
final X509Certificate trusted = trustStore.getEndEntityCertificate( session.getConnection().getPeerCertificates() );
boolean haveTrustedCertificate = trusted != null;
if (trusted != null && session.getDefaultIdentity() != null) {
......@@ -409,9 +403,8 @@ public class SASLAuthentication {
public static boolean verifyCertificates(Certificate[] chain, String hostname, boolean isS2S) {
final CertificateStoreManager certificateStoreManager = XMPPServer.getInstance().getCertificateStoreManager();
final ConnectionType connectionType = isS2S ? ConnectionType.SOCKET_S2S : ConnectionType.SOCKET_C2S;
final KeyStore keyStore = certificateStoreManager.getIdentityStore( connectionType ).getStore();
final KeyStore trustStore = certificateStoreManager.getTrustStore( connectionType ).getStore();
final X509Certificate trusted = CertificateManager.getEndEntityCertificate( chain, keyStore, trustStore );
final TrustStore trustStore = certificateStoreManager.getTrustStore( connectionType );
final X509Certificate trusted = trustStore.getEndEntityCertificate( chain );
if (trusted != null) {
return verifyCertificate(trusted, hostname);
}
......
......@@ -2,6 +2,7 @@ package org.jivesoftware.openfire.sasl;
import org.jivesoftware.openfire.Connection;
import org.jivesoftware.openfire.auth.AuthorizationManager;
import org.jivesoftware.openfire.keystore.TrustStore;
import org.jivesoftware.openfire.session.LocalClientSession;
import org.jivesoftware.util.CertificateManager;
import org.slf4j.Logger;
......@@ -12,7 +13,6 @@ import javax.security.sasl.SaslException;
import javax.security.sasl.SaslServer;
import java.nio.charset.StandardCharsets;
import java.security.cert.Certificate;
import java.security.KeyStore;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
......@@ -64,9 +64,8 @@ public class ExternalClientSaslServer implements SaslServer
throw new SaslException( "No peer certificates." );
}
final KeyStore keyStore = connection.getConfiguration().getIdentityStore().getStore();
final KeyStore trustStore = connection.getConfiguration().getTrustStore().getStore();
final X509Certificate trusted = CertificateManager.getEndEntityCertificate( peerCertificates, keyStore, trustStore );
final TrustStore trustStore = connection.getConfiguration().getTrustStore();
final X509Certificate trusted = trustStore.getEndEntityCertificate( peerCertificates );
if ( trusted == null )
{
throw new SaslException( "Certificate chain of peer is not trusted." );
......
......@@ -367,12 +367,7 @@ public class LocalIncomingServerSession extends LocalServerSession implements In
if (chain == null || chain.length == 0) {
usingSelfSigned = true;
} else {
try {
usingSelfSigned = CertificateManager.isSelfSignedCertificate((X509Certificate) chain[0]);
} catch (KeyStoreException ex) {
Log.warn("Exception occurred while trying to determine whether local certificate is self-signed. Proceeding as if it is.", ex);
usingSelfSigned = true;
}
}
if (usingSelfSigned && ServerDialback.isEnabledForSelfSigned() && validatedDomains.isEmpty()) {
......
......@@ -10,7 +10,7 @@ package org.jivesoftware.openfire.spi;
*/
public abstract class ConnectionAcceptor
{
protected final ConnectionConfiguration configuration;
protected ConnectionConfiguration configuration;
/**
* Constructs a new instance which will accept new connections based on the provided configuration.
......@@ -37,7 +37,7 @@ public abstract class ConnectionAcceptor
* An invocation of this method on an instance that is already started should have no effect (to the extend that the
* instance should continue to accept connections without interruption or configuration changes).
*/
abstract void start();
abstract public void start();
/**
* Halts connection acceptation and gracefully releases resources.
......@@ -47,12 +47,25 @@ public abstract class ConnectionAcceptor
* Instances of this class do not support configuration changes (see class documentation). As a result, there is no
* requirement that an instance that is stopped after it was running can successfully be restarted.
*/
abstract void stop();
abstract public void stop();
/**
* Determines if this instance is currently in a state where it is actively serving connections.
*
* @return false when this instance is started and is currently being used to serve connections (otherwise true)
*/
abstract boolean isIdle();
abstract public boolean isIdle();
/**
* Reloads the acceptor configuration, without causing a disconnect of already established connections.
*
* A best-effort reload will be attempted. Configuration changes that require a restart of connections,
* such as the bind-address and port, will not be applied.
*
* The configuration that's provided will replace the existing configuration. A restart of the connection
* acceptor after this method was invoked will apply all configuration changes.
*
* @param configuration The configuration for connections to be accepted (cannot be null).
*/
abstract public void reconfigure( ConnectionConfiguration configuration );
}
......@@ -314,7 +314,23 @@ public class ConnectionListener
{
start(); // won't actually start anything if not enabled.
}
Log.debug( "Done restarting..." );
Log.info( "Done restarting..." );
}
/**
* Reconfigures the acceptor without breaking existing connections. Note that not all configuration changes
* can be applied. These changes will be applied after a restart.
*/
public synchronized void reloadConfiguration()
{
if ( connectionAcceptor == null )
{
return; // There's no point in reloading config of a stopped instance. Config will be reloaded when started.
}
Log.debug( "Reconfiguring..." );
connectionAcceptor.reconfigure( generateConnectionConfiguration() );
Log.info( "Reconfigured." );
}
/**
......
......@@ -27,6 +27,7 @@ import org.jivesoftware.openfire.container.BasicModule;
import org.jivesoftware.openfire.container.PluginManager;
import org.jivesoftware.openfire.container.PluginManagerListener;
import org.jivesoftware.openfire.http.HttpBindManager;
import org.jivesoftware.openfire.keystore.CertificateStore;
import org.jivesoftware.openfire.keystore.CertificateStoreManager;
import org.jivesoftware.openfire.net.SocketSendingTracker;
import org.jivesoftware.openfire.session.ConnectionSettings;
......@@ -40,8 +41,6 @@ import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.security.KeyStore;
import java.security.cert.X509Certificate;
import java.util.*;
public class ConnectionManagerImpl extends BasicModule implements ConnectionManager, CertificateEventListener, PropertyEventListener
......@@ -576,50 +575,22 @@ public class ConnectionManagerImpl extends BasicModule implements ConnectionMana
// Certificates events
// #####################################################################
public void certificateCreated(KeyStore keyStore, String alias, X509Certificate cert) {
// Note that all non-SSL listeners can be using TLS - these also need to be restarted.
for ( final ConnectionListener listener : getListeners() )
{
// TODO determine by purpose exactly what needs and what need not be restarted.
try
{
listener.restart();
}
catch ( RuntimeException ex )
@Override
public void storeContentChanged( CertificateStore store )
{
Log.error( "An exception occurred while restarting listener " + listener + ". The reason for restart was a certificate store change.", ex );
}
}
}
public void certificateDeleted(KeyStore keyStore, String alias) {
// Note that all non-SSL listeners can be using TLS - these also need to be restarted.
for ( final ConnectionListener listener : getListeners() )
{
// TODO determine by purpose exactly what needs and what need not be restarted.
try
{
listener.restart();
}
catch ( RuntimeException ex )
if ( listener.getIdentityStoreConfiguration().equals( store.getConfiguration() ) || listener.getTrustStoreConfiguration().equals( store.getConfiguration() ) )
{
Log.error( "An exception occurred while restarting listener " + listener + ". The reason for restart was a certificate store change.", ex );
}
}
}
public void certificateSigned(KeyStore keyStore, String alias, List<X509Certificate> certificates) {
// Note that all non-SSL listeners can be using TLS - these also need to be restarted.
for ( final ConnectionListener listener : getListeners() )
{
// TODO determine by purpose exactly what needs and what need not be restarted.
try
{
listener.restart();
listener.reloadConfiguration();
}
catch ( RuntimeException ex )
{
Log.error( "An exception occurred while restarting listener " + listener + ". The reason for restart was a certificate store change.", ex );
Log.error( "An exception occurred while reloading listener " + listener + ". The reason for the reload was a certificate store change.", ex );
}
}
}
}
......
......@@ -87,8 +87,16 @@ public class LegacyConnectionAcceptor extends ConnectionAcceptor
}
@Override
boolean isIdle()
public synchronized boolean isIdle()
{
return socketAcceptThread != null; // We're not tracking actual sessions. This is a best effort response.
}
@Override
public synchronized void reconfigure( ConnectionConfiguration configuration )
{
this.configuration = configuration;
// nothing can be reloaded in this implementation.
}
}
......@@ -27,6 +27,10 @@ import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import java.lang.management.ManagementFactory;
import java.net.InetSocketAddress;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
......@@ -169,11 +173,66 @@ class MINAConnectionAcceptor extends ConnectionAcceptor
* @return false when this instance is started and is currently being used to serve connections (otherwise true)
*/
@Override
public boolean isIdle()
public synchronized boolean isIdle()
{
return this.socketAcceptor != null && this.socketAcceptor.getManagedSessionCount() == 0;
}
@Override
public synchronized void reconfigure( ConnectionConfiguration configuration )
{
this.configuration = configuration;
if ( socketAcceptor == null )
{
return; // reconfig will occur when acceptor is started.
}
final DefaultIoFilterChainBuilder filterChain = socketAcceptor.getFilterChain();
if ( filterChain.contains( ConnectionManagerImpl.EXECUTOR_FILTER_NAME ) )
{
final ExecutorFilter executorFilter = (ExecutorFilter) filterChain.get( ConnectionManagerImpl.EXECUTOR_FILTER_NAME );
( (ThreadPoolExecutor) executorFilter.getExecutor()).setCorePoolSize( ( configuration.getMaxThreadPoolSize() / 4 ) + 1 );
( (ThreadPoolExecutor) executorFilter.getExecutor()).setMaximumPoolSize( ( configuration.getMaxThreadPoolSize() ) );
}
if ( configuration.getTlsPolicy() == Connection.TLSPolicy.legacyMode )
{
// add or replace TLS filter (that's used only for 'direct-TLS')
try
{
final SslFilter sslFilter = encryptionArtifactFactory.createServerModeSslFilter();
if ( filterChain.contains( ConnectionManagerImpl.TLS_FILTER_NAME ) )
{
filterChain.replace( ConnectionManagerImpl.TLS_FILTER_NAME, sslFilter );
}
else
{
filterChain.addAfter( ConnectionManagerImpl.EXECUTOR_FILTER_NAME, ConnectionManagerImpl.TLS_FILTER_NAME, sslFilter );
}
}
catch ( KeyManagementException | NoSuchAlgorithmException | UnrecoverableKeyException | KeyStoreException e )
{
Log.error( "An exception occurred while reloading the TLS configuration.", e );
}
}
else
{
// The acceptor is in 'startTLS' mode. Remove TLS filter (that's used only for 'direct-TLS')
if ( filterChain.contains( ConnectionManagerImpl.TLS_FILTER_NAME ) )
{
filterChain.remove( ConnectionManagerImpl.TLS_FILTER_NAME );
}
}
if ( configuration.getMaxBufferSize() > 0 )
{
socketAcceptor.getSessionConfig().setMaxReadBufferSize( configuration.getMaxBufferSize() );
Log.debug( "Throttling read buffer for connections to max={} bytes", configuration.getMaxBufferSize() );
}
}
public synchronized int getPort()
{
return configuration.getPort();
......
......@@ -16,6 +16,8 @@
package org.jivesoftware.util;
import org.jivesoftware.openfire.keystore.CertificateStore;
import java.security.KeyStore;
import java.security.cert.X509Certificate;
import java.util.List;
......@@ -27,30 +29,11 @@ import java.util.List;
* @author Gaston Dombiak
*/
public interface CertificateEventListener {
/**
* Event triggered when a new certificate is created.
*
* @param keyStore key store where the certificate has been added.
* @param alias the alias of the certificate in the keystore.
* @param cert the new certificate created.
*/
void certificateCreated(KeyStore keyStore, String alias, X509Certificate cert);
/**
* Event triggered when a certificate is being deleted from the keystore.
*
* @param keyStore key store where the certificate is being deleted.
* @param alias the alias of the certificate in the keystore.
*/
void certificateDeleted(KeyStore keyStore, String alias);
/**
* Event triggered when a certificate has been signed by a Certificate Authority.
* Event triggered when the content of a certificate store was changed.
*
* @param keyStore key store where the certificate is stored.
* @param alias the alias of the certificate in the keystore.
* @param certificates chain of certificates. First certificate in the list is the certificate
* being signed and last certificate in the list is the root certificate.
* @param store The store for which the content was changed.
*/
void certificateSigned(KeyStore keyStore, String alias, List<X509Certificate> certificates);
void storeContentChanged( CertificateStore store );
}
......@@ -16,50 +16,6 @@
package org.jivesoftware.util;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringWriter;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchProviderException;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.cert.CertPath;
import java.security.cert.CertPathBuilder;
import java.security.cert.CertPathBuilderException;
import java.security.cert.CertPathValidator;
import java.security.cert.CertPathValidatorException;
import java.security.cert.CertStore;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.CollectionCertStoreParameters;
import java.security.cert.PKIXBuilderParameters;
import java.security.cert.X509CertSelector;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.bouncycastle.asn1.*;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.asn1.x500.X500Name;
......@@ -73,11 +29,7 @@ import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.openssl.MiscPEMGenerator;
import org.bouncycastle.openssl.PEMDecryptorProvider;
import org.bouncycastle.openssl.PEMEncryptedKeyPair;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.*;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8DecryptorProviderBuilder;
import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;
......@@ -101,6 +53,18 @@ import org.jivesoftware.util.cert.SANCertificateIdentityMapping;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Utility class that provides similar functionality to the keytool tool. Generated certificates
* conform to the XMPP spec where domains are kept in the subject alternative names extension.
......@@ -113,7 +77,6 @@ public class CertificateManager {
private static Pattern valuesPattern = Pattern.compile("(?i)(=)([^,]*)");
private static List<CertificateEventListener> listeners = new CopyOnWriteArrayList<>();
private static List<CertificateIdentityMapping> serverCertMapping = new ArrayList<>();
......@@ -170,119 +133,6 @@ public class CertificateManager {
}
}
/**
* Decide whether or not to trust the given supplied certificate chain, returning the
* End Entity Certificate in this case where it can, and null otherwise.
* A self-signed certificate will, for example, return null.
* For certain failures, we SHOULD generate an exception - revocations and the like,
* but we currently do not.
*
* @param chain an array of X509Certificate where the first one is the endEntityCertificate.
* @param certStore a keystore containing untrusted certificates (including ICAs, etc).
* @param trustStore a keystore containing Trust Anchors (most-trusted CA certificates).
* @return trusted end-entity certificate, or null.
*/
public static X509Certificate getEndEntityCertificate(Certificate chain[],
KeyStore certStore, KeyStore trustStore) {
if (chain == null || chain.length == 0) {
return null;
}
X509Certificate first = (X509Certificate) chain[0];
try {
first.checkValidity();
} catch(CertificateException e) {
Log.warn("EE Certificate not valid: " + e.getMessage());
return null;
}
if (chain.length == 1
&& first.getSubjectX500Principal().equals(first.getIssuerX500Principal())) {
// Chain is single cert, and self-signed.
try {
if (trustStore.getCertificateAlias(first) != null) {
// Interesting case: trusted self-signed cert.
return first;
}
} catch (KeyStoreException e) {
Log.warn("Keystore error while looking for self-signed cert; assuming untrusted.");
}
return null;
}
final List<Certificate> all_certs = new ArrayList<>();
try {
// First, load up certStore contents into a CertStore.
// It's a mystery why these objects are different.
for (Enumeration<String> aliases = certStore.aliases(); aliases
.hasMoreElements();) {
String alias = aliases.nextElement();
if (certStore.isCertificateEntry(alias)) {
X509Certificate cert = (X509Certificate) certStore
.getCertificate(alias);
all_certs.add(cert);
}
}
// Now add the trusted certs.
for (Enumeration<String> aliases = trustStore.aliases(); aliases
.hasMoreElements();) {
String alias = aliases.nextElement();
if (trustStore.isCertificateEntry(alias)) {
X509Certificate cert = (X509Certificate) trustStore
.getCertificate(alias);
all_certs.add(cert);
}
}
// Finally, add all the certs in the chain:
for (int i = 0; i < chain.length; ++i) {
all_certs.add(chain[i]);
}
CertStore cs = CertStore.getInstance("Collection",
new CollectionCertStoreParameters(all_certs));
X509CertSelector selector = new X509CertSelector();
selector.setCertificate(first);
// / selector.setSubject(first.getSubjectX500Principal());
PKIXBuilderParameters params = new PKIXBuilderParameters(
trustStore, selector);
params.addCertStore(cs);
params.setDate(new Date());
params.setRevocationEnabled(false);
/* Code here is the right way to do things. */
CertPathBuilder pathBuilder = CertPathBuilder
.getInstance(CertPathBuilder.getDefaultType());
CertPath cp = pathBuilder.build(params).getCertPath();
/*
* This section is an alternative to using CertPathBuilder which is
* not as complete (or safe), but will emit much better errors. If
* things break, swap around the code.
*
**** COMMENTED OUT. ****
ArrayList<X509Certificate> ls = new ArrayList<X509Certificate>();
for (int i = 0; i < chain.length; ++i) {
ls.add((X509Certificate) chain[i]);
}
for (X509Certificate last = ls.get(ls.size() - 1); !last
.getIssuerX500Principal().equals(last.getSubjectX500Principal()); last = ls
.get(ls.size() - 1)) {
X509CertSelector sel = new X509CertSelector();
sel.setSubject(last.getIssuerX500Principal());
ls.add((X509Certificate) cs.getCertificates(sel).toArray()[0]);
}
CertPath cp = CertificateFactory.getInstance("X.509").generateCertPath(ls);
****** END ALTERNATIVE. ****
*/
// Not entirely sure if I need to do this with CertPathBuilder.
// Can't hurt.
CertPathValidator pathValidator = CertPathValidator
.getInstance("PKIX");
pathValidator.validate(cp, params);
return (X509Certificate) cp.getCertificates().get(0);
} catch (CertPathBuilderException e) {
Log.warn("Path builder: " + e.getMessage());
} catch (CertPathValidatorException e) {
Log.warn("Path validator: " + e.getMessage());
} catch (Exception e) {
Log.warn("Unkown exception while validating certificate chain: " + e.getMessage());
}
return null;
}
/**
* Returns the identities of the remote client as defined in the specified certificate. The
......@@ -333,85 +183,12 @@ public class CertificateManager {
return names;
}
/**
* Returns true if an RSA certificate was found in the specified keystore for the specified domain.
*
* @param storeConfig the store to use for searching the certificate.
* @param domain domain of the server signed by the certificate.
* @return true if an RSA certificate was found in the specified keystore for the specified domain.
* @throws KeyStoreException
*/
public static boolean isRSACertificate(CertificateStore storeConfig, String domain) throws KeyStoreException {
return isCertificate(storeConfig, domain, "RSA");
}
/**
* Returns true if an DSA certificate was found in the specified keystore for the specified domain.
*
* @param storeConfig the store to use for searching the certificate.
* @param domain domain of the server signed by the certificate.
* @return true if an DSA certificate was found in the specified keystore for the specified domain.
* @throws KeyStoreException
*/
public static boolean isDSACertificate(CertificateStore storeConfig, String domain) throws KeyStoreException {
return isCertificate( storeConfig, domain, "DSA" );
}
/**
* Returns true if the specified certificate is using the DSA algorithm. The DSA algorithm is not
* good for encryption but only for authentication. On the other hand, the RSA algorithm is good
* for encryption and authentication.
*
* @param certificate the certificate to analyze.
* @return true if the specified certificate is using the DSA algorithm.
* @throws KeyStoreException
*/
public static boolean isDSACertificate(X509Certificate certificate) throws KeyStoreException {
return certificate.getPublicKey().getAlgorithm().equals( "DSA" );
}
/**
* Returns true if a certificate with the specified configuration was found in a certificate store.
*
* @param storeConfig the store to use for searching the certificate.
* @param domain the domain present in the subjectAltName or "*" if anything is accepted.
* @param algorithm the DSA or RSA algorithm used by the certificate.
* @return true if a certificate with the specified configuration was found in the key store.
* @throws KeyStoreException
*/
private static boolean isCertificate(CertificateStore storeConfig, String domain, String algorithm) throws KeyStoreException {
for (Enumeration<String> aliases = storeConfig.getStore().aliases(); aliases.hasMoreElements();) {
X509Certificate certificate = (X509Certificate) storeConfig.getStore().getCertificate(aliases.nextElement());
if ( !certificate.getPublicKey().getAlgorithm().equalsIgnoreCase( algorithm ) ) {
continue;
}
if ("*".equals(domain)) {
// Any domain certified by the certificate is accepted
return true;
}
else {
// Only accept certified domains that match the specified domain
// TODO check that domain=foo.bar does not match identitiy "a.longerfoo.bar"
for (String identity : getServerIdentities( certificate ) ) {
if (identity.endsWith(domain) ) {
return true;
}
}
}
}
return false;
}
/**
* Returns true if the specified certificate is a self-signed certificate.
*
* @return true if the specified certificate is a self-signed certificate.
* @throws KeyStoreException if an error happens while usign the keystore
*/
public static boolean isSelfSignedCertificate(X509Certificate certificate) throws KeyStoreException {
public static boolean isSelfSignedCertificate(X509Certificate certificate) {
try {
certificate.verify(certificate.getPublicKey());
return true;
......@@ -426,9 +203,8 @@ public class CertificateManager {
* Signing Request (CSR).
*
* @return true if the specified certificate is ready to be signed by a Certificate Authority.
* @throws KeyStoreException if an error happens while usign the keystore
*/
public static boolean isSigningRequestPending(X509Certificate certificate) throws KeyStoreException {
public static boolean isSigningRequestPending(X509Certificate certificate) {
// Verify that this is a self-signed certificate
if (!isSelfSignedCertificate(certificate)) {
return false;
......@@ -443,8 +219,7 @@ public class CertificateManager {
* requests are required by Certificate Authorities as part of their signing process. The signing request
* contains information about the certificate issuer, subject DN, subject alternative names and public key.
* Private keys are not included. After the Certificate Authority verified and signed the certificate a new
* certificate is going to be returned. Use {@link #installReply(java.security.KeyStore, java.security.KeyStore, char[], String, java.io.InputStream)}
* to import the CA reply.
* certificate is going to be returned.
*
* @param cert the certificate to create a signing request.
* @param privKey the private key of the certificate.
......@@ -472,130 +247,6 @@ public class CertificateManager {
return string.toString();
}
/**
* Installs the Certificate Authority reply returned as part of the signing request. The certificate
* being signed will get its certificate chain updated with the imported certificate(s). An exception
* will be thrown if the replied certificate does not match a local certificate or if the signing
* authority is not known by the server (i.e. keystore and truststore files)
*
* The identity of the entity that has signed the reply is verified against the provided trust store.
*
* The
*
* @param keyStore key store where the certificate is stored.
* @param trustStore key store where ca certificates are stored.
* @param keyPassword password of the keystore.
* @param alias the alias of the existing certificate being signed.
* @param inputStream the stream containing the CA reply.
* @return true if the CA reply was successfully processed.
* @throws Exception
*/
public static boolean installReply(KeyStore keyStore, KeyStore trustStore, char[] keyPassword, String alias, InputStream inputStream) throws Exception {
// Check that there is a certificate for the specified alias
X509Certificate certificate = (X509Certificate) keyStore.getCertificate( alias );
if (certificate == null) {
Log.warn("Certificate not found for alias: " + alias);
return false;
}
// Retrieve the private key of the stored certificate
PrivateKey privKey = (PrivateKey) keyStore.getKey(alias, keyPassword);
// Load certificates found in the PEM input stream
Collection<X509Certificate> certs = parseCertificates( inputStream );
if (certs.isEmpty()) {
throw new Exception("Reply has no certificates");
}
List<X509Certificate> newCerts;
if (certs.size() == 1) {
// Reply has only one certificate
newCerts = establishCertChain(keyStore, trustStore, null, certs.iterator().next());
} else {
// Reply has a chain of certificates
newCerts = validateReply(keyStore, trustStore, alias, null, certs);
}
if (newCerts == null)
{
return false;
}
keyStore.setKeyEntry(alias, privKey, keyPassword, newCerts.toArray(new X509Certificate[newCerts.size()]));
// Notify listeners that a new certificate has been created
for (CertificateEventListener listener : listeners) {
try {
listener.certificateSigned( keyStore, alias, newCerts );
}
catch (Exception e) {
Log.error(e.getMessage(), e);
}
}
return true;
}
/**
* Imports a new signed certificate and its private key into the keystore. The certificate input
* stream may contain the signed certificate as well as its CA chain.
*
* @param keyStore key store where the certificate will be stored.
* @param trustStore key store where ca certificates are stored.
* @param keyPassword password of the keystore.
* @param alias the alias of the the new signed certificate.
* @param pkInputStream the stream containing the private key.
* @param passPhrase is the password phrased used when creating the private key.
* @param inputStream the stream containing the signed certificate.
* @return true if the certificate was successfully imported.
* @throws Exception if no certificates were found in the inputStream.
*/
public static boolean installCert(KeyStore keyStore, KeyStore trustStore, String keyPassword, String alias,
InputStream pkInputStream, final String passPhrase, InputStream inputStream) throws Exception {
// Check that there is a certificate for the specified alias
X509Certificate certificate = (X509Certificate) keyStore.getCertificate(alias);
if (certificate != null) {
Log.warn("Certificate already exists for alias: " + alias);
return false;
}
PrivateKey privKey = parsePrivateKey( pkInputStream, passPhrase );
Collection<X509Certificate> certs = parseCertificates( inputStream );
if (certs.isEmpty()) {
throw new Exception("No certificates were found");
}
List<X509Certificate> newCerts;
if (certs.size() == 1)
{
// Reply has only one certificate
newCerts = establishCertChain(keyStore, trustStore, certificate, certs.iterator().next() );
}
else
{
// Reply has a chain of certificates
newCerts = validateReply(keyStore, trustStore, alias, certificate, certs);
}
if (newCerts == null)
{
return false;
}
keyStore.setKeyEntry( alias, privKey, keyPassword.toCharArray(), newCerts.toArray( new X509Certificate[ newCerts.size() ] ) );
// Notify listeners that a new certificate has been created (and signed)
for (CertificateEventListener listener : listeners) {
try {
listener.certificateCreated( keyStore, alias, newCerts.get( 0 ) );
if (newCerts.size() > 1) {
listener.certificateSigned(keyStore, alias, newCerts);
}
}
catch (Exception e) {
Log.error(e.getMessage(), e);
}
}
return true;
}
public static PrivateKey parsePrivateKey(String pemRepresentation, String passPhrase) throws IOException {
if (pemRepresentation == null || pemRepresentation.trim().isEmpty()) {
......@@ -714,120 +365,22 @@ public class CertificateManager {
listeners.remove( listener );
}
private static List<X509Certificate> establishCertChain(KeyStore keyStore, KeyStore trustStore,
X509Certificate certificate,
X509Certificate certReply)
throws Exception {
if (certificate != null) {
PublicKey publickey = certificate.getPublicKey();
PublicKey publickey1 = certReply.getPublicKey();
if (!publickey.equals(publickey1)) {
throw new Exception("Public keys in reply and keystore don't match");
}
if (certReply.equals(certificate)) {
throw new Exception("Certificate reply and certificate in keystore are identical");
}
}
Map<String, List<X509Certificate>> knownCerts = new Hashtable<>();
// TODO Figure out why we add keystore issuers. This implies that we always trust the issuer of our identitity (which probably is right, but shouldn't be required)
if (keyStore.size() > 0) {
knownCerts.putAll(getCertsByIssuer(keyStore));
}
if (trustStore.size() > 0) {
knownCerts.putAll(getCertsByIssuer(trustStore));
}
LinkedList<X509Certificate> answer = new LinkedList<>();
if (buildChain(certReply, answer, knownCerts)) {
return answer;
} else {
throw new Exception("Failed to establish chain from reply");
}
}
/**
* Builds the certificate chain of the specified certificate based on the known list of certificates
* that were issued by their respective Principals. Returns true if the entire chain of all certificates
* was successfully built.
*
* @param certificate certificate to build its chain.
* @param answer the certificate chain for the corresponding certificate.
* @param knownCerts list of known certificates grouped by their issues (i.e. Principals).
* @return true if the entire chain of all certificates was successfully built.
*/
private static boolean buildChain(X509Certificate certificate, LinkedList<X509Certificate> answer,
Map<String, List<X509Certificate>> knownCerts) {
Principal subject = certificate.getSubjectDN();
Principal issuer = certificate.getIssuerDN();
// Check if the certificate is a root certificate (i.e. was issued by the same Principal that
// is present in the subject)
if (subject.equals(issuer)) {
answer.addFirst(certificate);
return true;
}
// Get the list of known certificates of the certificate's issuer
List<X509Certificate> issuerCerts = knownCerts.get(issuer.getName());
if (issuerCerts == null || issuerCerts.isEmpty()) {
// No certificates were found so building of chain failed
return false;
}
for (X509Certificate issuerCert : issuerCerts) {
PublicKey publickey = issuerCert.getPublicKey();
try {
// Verify the certificate with the specified public key
certificate.verify(publickey);
// Certificate was verified successfully so build chain of issuer's certificate
if (!buildChain(issuerCert, answer, knownCerts)) {
return false;
}
}
catch (Exception exception) {
// Failed to verify certificate
return false;
}
}
answer.addFirst( certificate );
return true;
}
/**
* Returns a Map where the key holds the certificate issuers and values the certificates of each issuer.
*
* @param ks the keystore to get its certs per issuer.
* @return a map with the certificates per issuer.
* @throws Exception
* Notify listeners that a certificate store has been changed.
*/
private static Map<String, List<X509Certificate>> getCertsByIssuer(KeyStore ks)
throws Exception {
Map<Principal, List<X509Certificate>> answer = new HashMap<>();
Enumeration<String> aliases = ks.aliases();
while (aliases.hasMoreElements()) {
String alias = aliases.nextElement();
X509Certificate cert = (X509Certificate) ks.getCertificate(alias);
if (cert != null) {
Principal subjectDN = cert.getSubjectDN();
List<X509Certificate> vec = answer.get(subjectDN);
if (vec == null) {
vec = new ArrayList<>();
vec.add(cert);
}
else {
if (!vec.contains(cert)) {
vec.add(cert);
}
}
answer.put(subjectDN, vec);
}
public static void fireCertificateStoreChanged( CertificateStore store )
{
for ( CertificateEventListener listener : listeners )
{
try
{
listener.storeContentChanged( store );
}
// Compare by principal, but return by principal name.
final Map<String, List<X509Certificate>> result = new HashMap<>();
for ( Map.Entry<Principal, List<X509Certificate>> entry : answer.entrySet() )
catch ( Exception e )
{
result.put( entry.getKey().getName(), entry.getValue() );
Log.error( "A listener threw an exception while processing a 'store changed' event.", e );
}
}
return result;
}
/**
......@@ -848,108 +401,6 @@ public class CertificateManager {
return CertificateUtils.order( certificates );
}
/**
* Validates chain in certification reply, and returns the ordered
* elements of the chain (with user certificate first, and root
* certificate last in the array).
*
* @param alias the alias name
* @param userCert the user certificate of the alias
* @param certs the chain provided in the reply
*/
private static List<X509Certificate> validateReply(KeyStore keyStore, KeyStore trustStore, String alias,
X509Certificate userCert, Collection<X509Certificate> certs)
throws Exception {
List<X509Certificate> replyCerts = new ArrayList<>(certs);
// order the certs in the reply (bottom-up).
int i;
X509Certificate tmpCert;
if (userCert != null) {
PublicKey userPubKey = userCert.getPublicKey();
for (i = 0; i < replyCerts.size(); i++) {
if (userPubKey.equals(replyCerts.get(i).getPublicKey())) {
break;
}
}
if (i == replyCerts.size()) {
throw new Exception(
"Certificate reply does not contain public key for <alias>: " + alias);
}
tmpCert = replyCerts.get(0);
replyCerts.set(0, replyCerts.get(i));
replyCerts.set(i, tmpCert);
}
Principal issuer = replyCerts.get(0).getIssuerDN();
for (i = 1; i < replyCerts.size() - 1; i++) {
// find a cert in the reply whose "subject" is the same as the
// given "issuer"
int j;
for (j = i; j < replyCerts.size(); j++) {
Principal subject = replyCerts.get(j).getSubjectDN();
if (subject.equals(issuer)) {
tmpCert = replyCerts.get(i);
replyCerts.set(i, replyCerts.get(j));
replyCerts.set(j, tmpCert);
issuer = replyCerts.get(i).getIssuerDN();
break;
}
}
if (j == replyCerts.size()) {
throw new Exception("Incomplete certificate chain in reply");
}
}
// now verify each cert in the ordered chain
for (i = 0; i < replyCerts.size() - 1; i++) {
PublicKey pubKey = replyCerts.get(i + 1).getPublicKey();
try {
replyCerts.get(i).verify(pubKey);
}
catch (Exception e) {
throw new Exception(
"Certificate chain in reply does not verify: " + e.getMessage());
}
}
// do we trust the (root) cert at the top?
X509Certificate topCert = replyCerts.get(replyCerts.size() - 1);
boolean foundInKeyStore = keyStore.getCertificateAlias(topCert) != null;
boolean foundInCAStore = trustStore.getCertificateAlias(topCert) != null;
if (!foundInKeyStore && !foundInCAStore) {
boolean verified = false;
X509Certificate rootCert = null;
for (Enumeration<String> aliases = trustStore.aliases(); aliases.hasMoreElements();) {
String name = aliases.nextElement();
rootCert = (X509Certificate) trustStore.getCertificate(name);
if (rootCert != null) {
try {
topCert.verify(rootCert.getPublicKey());
verified = true;
break;
}
catch (Exception e) {
// Ignore
}
}
}
if (!verified) {
return null;
}
else {
// Check if the cert is a self-signed cert
if (!topCert.getSubjectDN().equals(topCert.getIssuerDN())) {
// append the (self-signed) root CA cert to the chain
replyCerts.add(rootCert);
}
}
}
return replyCerts;
}
/**
* Creates an X509 version3 certificate.
*
......
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