Commit 88852760 authored by Guus der Kinderen's avatar Guus der Kinderen

OF-1438: Add method to replace an entry in the identity store.

parent 92293147
...@@ -14,10 +14,7 @@ import java.security.*; ...@@ -14,10 +14,7 @@ import java.security.*;
import java.security.cert.Certificate; import java.security.cert.Certificate;
import java.security.cert.CertificateException; import java.security.cert.CertificateException;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.util.Arrays; import java.util.*;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/** /**
* A wrapper class for a store of certificates, its metadata (password, location) and related functionality that is * A wrapper class for a store of certificates, its metadata (password, location) and related functionality that is
...@@ -204,7 +201,8 @@ public class IdentityStore extends CertificateStore ...@@ -204,7 +201,8 @@ public class IdentityStore extends CertificateStore
} }
/** /**
* Imports a certificate and the private key that was used to generate the certificate. * Imports a certificate and the private key that was used to generate the certificate, replacing any previously
* installed entries for the same domain.
* *
* This method will import the certificate and key in the store using a unique alias. This alias is returned. * This method will import the certificate and key in the store using a unique alias. This alias is returned.
* *
...@@ -215,25 +213,76 @@ public class IdentityStore extends CertificateStore ...@@ -215,25 +213,76 @@ public class IdentityStore extends CertificateStore
* @param passPhrase optional pass phrase (must be present if the private key is encrypted). * @param passPhrase optional pass phrase (must be present if the private key is encrypted).
* @return The alias that was used (never null). * @return The alias that was used (never null).
*/ */
public String installCertificate( String pemCertificates, String pemPrivateKey, String passPhrase ) throws CertificateStoreConfigException public String replaceCertificate( String pemCertificates, String pemPrivateKey, String passPhrase ) throws CertificateStoreConfigException
{ {
// Generate a unique alias. if ( pemCertificates == null || pemCertificates.trim().isEmpty() )
final String domain = XMPPServer.getInstance().getServerInfo().getXMPPDomain(); {
int index = 1; throw new IllegalArgumentException( "Argument 'pemCertificates' cannot be null or an empty String." );
String alias = domain + "_" + index; }
if ( pemPrivateKey == null || pemPrivateKey.trim().isEmpty() )
{
throw new IllegalArgumentException( "Argument 'pemPrivateKey' cannot be null or an empty String." );
}
pemCertificates = pemCertificates.trim();
try try
{ {
while ( store.containsAlias( alias ) ) // From its PEM representation, parse the certificates.
final Collection<X509Certificate> certificates = CertificateManager.parseCertificates( pemCertificates );
if ( certificates.isEmpty() )
{ {
index = index + 1; throw new CertificateStoreConfigException( "No certificate was found in the input." );
alias = domain + "_" + index;
} }
// Note that PKCS#7 does not require a specific order for the certificates in the file - ordering is needed.
final List<X509Certificate> ordered = CertificateUtils.order( certificates );
// Of the ordered chain, the first certificate should be for our domain.
if ( !isForThisDomain( ordered.get( 0 ) ) )
{
throw new CertificateStoreConfigException( "The supplied certificate chain does not cover the domain of this XMPP service." );
}
// From its PEM representation (and pass phrase), parse the private key.
final PrivateKey privateKey = CertificateManager.parsePrivateKey( pemPrivateKey, passPhrase );
// All appears to be in order. Replace any entries in the store.
removeAllDomainEntries();
final String alias = generateUniqueAlias();
store.setKeyEntry( alias, privateKey, configuration.getPassword(), ordered.toArray( new X509Certificate[ ordered.size() ] ) );
persist();
Log.info( "Replaced all private keys and corresponding certificate chains with a new private key and certificate chain." );
return alias;
} }
catch ( KeyStoreException e ) catch ( CertificateException | KeyStoreException | IOException e )
{ {
reload(); // reset state of the store.
throw new CertificateStoreConfigException( "Unable to install a certificate into an identity store.", e ); throw new CertificateStoreConfigException( "Unable to install a certificate into an identity store.", e );
} }
// TODO Notify listeners that a new certificate has been replaced.
}
/**
* Imports a certificate and the private key that was used to generate the certificate.
*
* This method will import the certificate and key in the store using a unique alias. This alias is returned.
*
* This method will fail when the provided certificate does not match the domain of this XMPP service.
*
* @param pemCertificates a PEM representation of the certificate or certificate chain (cannot be null or empty).
* @param pemPrivateKey a PEM representation of the private key (cannot be null or empty).
* @param passPhrase optional pass phrase (must be present if the private key is encrypted).
* @return The alias that was used (never null).
*/
public String installCertificate( String pemCertificates, String pemPrivateKey, String passPhrase ) throws CertificateStoreConfigException
{
final String alias = generateUniqueAlias();
// Perform the installation using the generated alias. // Perform the installation using the generated alias.
installCertificate( alias, pemCertificates, pemPrivateKey, passPhrase ); installCertificate( alias, pemCertificates, pemPrivateKey, passPhrase );
...@@ -300,6 +349,9 @@ public class IdentityStore extends CertificateStore ...@@ -300,6 +349,9 @@ public class IdentityStore extends CertificateStore
store.setKeyEntry( alias, privateKey, configuration.getPassword(), ordered.toArray( new X509Certificate[ ordered.size() ] ) ); store.setKeyEntry( alias, privateKey, configuration.getPassword(), ordered.toArray( new X509Certificate[ ordered.size() ] ) );
persist(); persist();
Log.info( "Installed a new private key and corresponding certificate chain." );
} }
catch ( CertificateException | KeyStoreException | IOException e ) catch ( CertificateException | KeyStoreException | IOException e )
{ {
...@@ -473,4 +525,65 @@ public class IdentityStore extends CertificateStore ...@@ -473,4 +525,65 @@ public class IdentityStore extends CertificateStore
Log.info( "The supplied certificate chain does not cover the domain of this XMPP service ('" + domainName + "'). Instead, it covers " + Arrays.toString( serverIdentities.toArray( new String[ serverIdentities.size() ] ) ) ); Log.info( "The supplied certificate chain does not cover the domain of this XMPP service ('" + domainName + "'). Instead, it covers " + Arrays.toString( serverIdentities.toArray( new String[ serverIdentities.size() ] ) ) );
return false; return false;
} }
/**
* Generates an alias that is currently unused in this store.
*
* @return An alias (never null).
*/
protected synchronized String generateUniqueAlias() throws CertificateStoreConfigException
{
final String domain = XMPPServer.getInstance().getServerInfo().getXMPPDomain();
int index = 1;
String alias = domain + "_" + index;
try
{
while ( store.containsAlias( alias ) )
{
index = index + 1;
alias = domain + "_" + index;
}
return alias;
}
catch ( KeyStoreException e )
{
throw new CertificateStoreConfigException( "Unable to generate a unique alias for this identity store.", e );
}
}
/**
* Removes all entries that reflect the local domain.
*
* This method iterates over all entries, and removes those that match the domain of this server.
*
* Note that the changes are not persisted by this method (as it is expected to be used in tandem with an insert.
*/
protected synchronized void removeAllDomainEntries() throws KeyStoreException
{
final String domainName = XMPPServer.getInstance().getServerInfo().getXMPPDomain();
final Set<String> toDelete = new HashSet<>();
for ( final String alias : Collections.list( store.aliases() ) )
{
final Certificate certificate = store.getCertificate( alias );
if ( !( certificate instanceof X509Certificate ) )
{
continue;
}
for ( String identity : CertificateManager.getServerIdentities( (X509Certificate) certificate ) )
{
if ( DNSUtil.isNameCoveredByPattern( domainName, identity ) )
{
toDelete.add( alias );
break;
}
}
}
for ( final String alias : toDelete )
{
store.deleteEntry( alias );
}
}
} }
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