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.*;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.*;
/**
* 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
}
/**
* 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.
*
......@@ -215,25 +213,76 @@ public class IdentityStore extends CertificateStore
* @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
public String replaceCertificate( String pemCertificates, String pemPrivateKey, String passPhrase ) throws CertificateStoreConfigException
{
// Generate a unique alias.
final String domain = XMPPServer.getInstance().getServerInfo().getXMPPDomain();
int index = 1;
String alias = domain + "_" + index;
if ( pemCertificates == null || pemCertificates.trim().isEmpty() )
{
throw new IllegalArgumentException( "Argument 'pemCertificates' cannot be null or an empty String." );
}
if ( pemPrivateKey == null || pemPrivateKey.trim().isEmpty() )
{
throw new IllegalArgumentException( "Argument 'pemPrivateKey' cannot be null or an empty String." );
}
pemCertificates = pemCertificates.trim();
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;
alias = domain + "_" + index;
throw new CertificateStoreConfigException( "No certificate was found in the input." );
}
// 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 );
}
// 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.
installCertificate( alias, pemCertificates, pemPrivateKey, passPhrase );
......@@ -300,6 +349,9 @@ public class IdentityStore extends CertificateStore
store.setKeyEntry( alias, privateKey, configuration.getPassword(), ordered.toArray( new X509Certificate[ ordered.size() ] ) );
persist();
Log.info( "Installed a new private key and corresponding certificate chain." );
}
catch ( CertificateException | KeyStoreException | IOException e )
{
......@@ -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() ] ) ) );
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