![JAR search and dependency download from the Maven repository](/logo.png)
org.jivesoftware.openfire.keystore.IdentityStore Maven / Gradle / Ivy
The newest version!
package org.jivesoftware.openfire.keystore;
import org.bouncycastle.operator.OperatorCreationException;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.net.DNSUtil;
import org.jivesoftware.util.CertificateManager;
import org.jivesoftware.util.JiveGlobals;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.KeyManagerFactory;
import java.io.IOException;
import java.security.*;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.*;
/**
* A wrapper class for a store of certificates, its metadata (password, location) and related functionality that is
* used to provide credentials (that represent this Openfire instance), an identity store
*
* An identity store should contain private keys, each associated with its certificate chain.
*
* Having the root certificate of the Certificate Authority that signed the certificates in this identity store should
* be in a corresponding trust store, although this is not strictly required. The reasoning here is that when you trust
* a Certificate Authority to verify your identity, you're likely to trust the same Certificate Authority to verify the
* identities of others.
*
* Note that in Java terminology, an identity store is commonly referred to as a 'key store', while the same name is
* also used to identify the generic certificate store. To have clear distinction between common denominator and each of
* the specific types, this implementation uses the terms "certificate store", "identity store" and "trust store".
*
* @author Guus der Kinderen, [email protected]
*/
public class IdentityStore extends CertificateStore
{
private static final Logger Log = LoggerFactory.getLogger( IdentityStore.class );
public IdentityStore( CertificateStoreConfiguration configuration, boolean createIfAbsent ) throws CertificateStoreConfigException
{
super( configuration, createIfAbsent );
try
{
final KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance( KeyManagerFactory.getDefaultAlgorithm() );
keyManagerFactory.init( this.getStore(), configuration.getPassword() );
}
catch ( NoSuchAlgorithmException | UnrecoverableKeyException | KeyStoreException ex )
{
throw new CertificateStoreConfigException( "Unable to initialize identity store (a common cause: the password for a key is different from the password of the entire store).", ex );
}
}
/**
* Creates a Certificate Signing Request based on the private key and certificate identified by the provided alias.
*
* When the alias does not identify a private key and/or certificate, this method will throw an exception.
*
* The certificate that is identified by the provided alias can be an unsigned certificate, but also a certificate
* that is already signed. The latter implies that the generated request is a request for certificate renewal.
*
* An invocation of this method does not change the state of the underlying store.
*
* @param alias An identifier for a private key / certificate in this store (cannot be null).
* @return A PEM-encoded Certificate Signing Request (never null).
*/
public String generateCSR( String alias ) throws CertificateStoreConfigException
{
// Input validation
if ( alias == null || alias.trim().isEmpty() )
{
throw new IllegalArgumentException( "Argument 'alias' cannot be null or an empty String." );
}
alias = alias.trim();
try
{
if ( !store.containsAlias( alias ) ) {
throw new CertificateStoreConfigException( "Cannot generate CSR for alias '"+ alias +"': the alias does not exist in the store." );
}
final Certificate certificate = store.getCertificate( alias );
if ( certificate == null || (!(certificate instanceof X509Certificate)))
{
throw new CertificateStoreConfigException( "Cannot generate CSR for alias '"+ alias +"': there is no corresponding certificate in the store, or it is not an X509 certificate." );
}
final Key key = store.getKey( alias, configuration.getPassword() );
if ( key == null || (!(key instanceof PrivateKey) ) )
{
throw new CertificateStoreConfigException( "Cannot generate CSR for alias '"+ alias +"': there is no corresponding key in the store, or it is not a private key." );
}
final String pemCSR = CertificateManager.createSigningRequest( (X509Certificate) certificate, (PrivateKey) key );
return pemCSR;
}
catch ( IOException | KeyStoreException | UnrecoverableKeyException | NoSuchAlgorithmException | OperatorCreationException e )
{
throw new CertificateStoreConfigException( "Cannot generate CSR for alias '"+ alias +"'", e );
}
}
/**
* Imports a certificate (and its chain) in this store.
*
* This method will fail when the provided certificate chain:
*
* - does not match the domain of this XMPP service.
* - is not a proper chain
*
*
* This method will also fail when a corresponding private key is not already in this store (it is assumed that the
* CA reply follows a signing request based on a private key that was added to the store earlier).
*
* @param pemCertificates a PEM representation of the certificate or certificate chain (cannot be null or empty).
*/
public void installCSRReply( String alias, String pemCertificates ) throws CertificateStoreConfigException
{
// Input validation
if ( alias == null || alias.trim().isEmpty() )
{
throw new IllegalArgumentException( "Argument 'alias' cannot be null or an empty String." );
}
if ( pemCertificates == null || pemCertificates.trim().isEmpty() )
{
throw new IllegalArgumentException( "Argument 'pemCertificates' cannot be null or an empty String." );
}
alias = alias.trim();
pemCertificates = pemCertificates.trim();
try
{
// From its PEM representation, parse the certificates.
final Collection certificates = CertificateManager.parseCertificates( pemCertificates );
if ( certificates.isEmpty() )
{
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 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." );
}
// This method is used to update a pre-existing entry in the store. Find out if this entry corresponds with the provided certificate chain.
if ( !corresponds( alias, ordered ) ) {
throw new IllegalArgumentException( "The provided CSR reply does not match an existing certificate in the store under the provided alias '" + alias + "'." );
}
// All appears to be in order. Update the existing entry in the store.
store.setKeyEntry( alias, store.getKey( alias, configuration.getPassword() ), configuration.getPassword(), ordered.toArray( new X509Certificate[ ordered.size() ] ) );
}
catch ( RuntimeException | IOException | CertificateException | UnrecoverableKeyException | KeyStoreException | NoSuchAlgorithmException e )
{
reload(); // reset state of the store.
throw new CertificateStoreConfigException( "Unable to install a singing reply into an identity store.", e );
}
// TODO notifiy listeners.
}
protected boolean corresponds( String alias, List certificates ) throws KeyStoreException, UnrecoverableKeyException, NoSuchAlgorithmException
{
if ( !store.containsAlias( alias ) ) {
return false;
}
final Key key = store.getKey( alias, configuration.getPassword() );
if ( key == null ) {
return false;
}
if ( !(key instanceof PrivateKey)) {
return false;
}
final Certificate certificate = store.getCertificate( alias );
if ( certificate == null ) {
return false;
}
if ( !(certificate instanceof X509Certificate) ) {
return false;
}
final X509Certificate x509Certificate = (X509Certificate) certificate;
// First certificate in the chain should correspond with the certificate in the store
if ( !x509Certificate.getPublicKey().equals(certificates.get(0).getPublicKey()) )
{
return false;
}
return true;
}
/**
* 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 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 replaceCertificate( String pemCertificates, String pemPrivateKey, String passPhrase ) throws CertificateStoreConfigException
{
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
{
// From its PEM representation, parse the certificates.
final Collection certificates = CertificateManager.parseCertificates( pemCertificates );
if ( certificates.isEmpty() )
{
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 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 ( 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 );
return alias;
}
/**
* Imports a certificate and the private key that was used to generate the certificate.
*
* This method will fail when the provided certificate does not match the domain of this XMPP service, or when the
* provided alias refers to an existing entry.
*
* @param alias the name (key) under which the certificate is to be stored in the store (cannot be null or empty).
* @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).
*/
public void installCertificate( String alias, String pemCertificates, String pemPrivateKey, String passPhrase ) throws CertificateStoreConfigException
{
// Input validation
if ( alias == null || alias.trim().isEmpty() )
{
throw new IllegalArgumentException( "Argument 'alias' cannot be null or an empty String." );
}
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." );
}
alias = alias.trim();
pemCertificates = pemCertificates.trim();
// Check that there is a certificate for the specified alias
try
{
if ( store.containsAlias( alias ) )
{
throw new CertificateStoreConfigException( "Certificate already exists for alias: " + alias );
}
// From its PEM representation, parse the certificates.
final Collection certificates = CertificateManager.parseCertificates( pemCertificates );
if ( certificates.isEmpty() )
{
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 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. Install in the store.
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 )
{
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 added.
}
/**
* Adds a self-signed certificate for the domain of this XMPP service when no certificate for the domain (of the
* provided algorithm) was found.
*
* This method is a thread-safe equivalent of:
*
* for ( String algorithm : algorithms ) {
* if ( !containsDomainCertificate( algorithm ) ) {
* addSelfSignedDomainCertificate( algorithm );
* }
* }
*
*
* @param algorithms The algorithms for which to verify / add a domain certificate.
*/
public synchronized void ensureDomainCertificates( String... algorithms ) throws CertificateStoreConfigException
{
for ( String algorithm : algorithms )
{
Log.debug( "Verifying that a domain certificate ({} algorithm) is available in this store.", algorithm);
if ( !containsDomainCertificate( algorithm ) )
{
Log.debug( "Store does not contain a domain certificate ({} algorithm). A self-signed certificate will be generated.", algorithm);
addSelfSignedDomainCertificate( algorithm );
}
}
}
/**
* Checks if the store contains a certificate of a particular algorithm that matches the domain of this
* XMPP service. This method will not distinguish between self-signed and non-self-signed certificates.
*/
public synchronized boolean containsDomainCertificate( String algorithm ) throws CertificateStoreConfigException
{
final String domainName = XMPPServer.getInstance().getServerInfo().getXMPPDomain();
try
{
for ( final String alias : Collections.list( store.aliases() ) )
{
final Certificate certificate = store.getCertificate( alias );
if ( !( certificate instanceof X509Certificate ) )
{
continue;
}
if ( !certificate.getPublicKey().getAlgorithm().equalsIgnoreCase( algorithm ) )
{
continue;
}
for ( String identity : CertificateManager.getServerIdentities( (X509Certificate) certificate ) )
{
if ( DNSUtil.isNameCoveredByPattern( domainName, identity ) )
{
return true;
}
}
}
return false;
}
catch ( KeyStoreException e )
{
throw new CertificateStoreConfigException( "An exception occurred while searching for " + algorithm + " certificates that match the Openfire domain.", e );
}
}
/**
* Populates the key store with a self-signed certificate for the domain of this XMPP service.
*/
public synchronized void addSelfSignedDomainCertificate( String algorithm ) throws CertificateStoreConfigException
{
final int keySize;
final String signAlgorithm;
switch ( algorithm.toUpperCase() )
{
case "RSA":
keySize = JiveGlobals.getIntProperty( "cert.rsa.keysize", 2048 );
signAlgorithm = "SHA256WITHRSAENCRYPTION";
break;
case "DSA":
keySize = JiveGlobals.getIntProperty( "cert.dsa.keysize", 1024 );
signAlgorithm = "SHA256withDSA";
break;
default:
throw new IllegalArgumentException( "Unsupported algorithm '" + algorithm + "'. Use 'RSA' or 'DSA'." );
}
final String name = JiveGlobals.getProperty( "xmpp.domain" ).toLowerCase();
final String alias = name + "_" + algorithm.toLowerCase();
final int validityInDays = 5*365;
Log.info( "Generating a new private key and corresponding self-signed certificate for domain name '{}', using the {} algorithm (sign-algorithm: {} with a key size of {} bits). Certificate will be valid for {} days.", name, algorithm, signAlgorithm, keySize, validityInDays );
// Generate public and private keys
try
{
final KeyPair keyPair = generateKeyPair( algorithm.toUpperCase(), keySize );
// Create X509 certificate with keys and specified domain
final X509Certificate cert = CertificateManager.createX509V3Certificate( keyPair, validityInDays, name, name, name, signAlgorithm );
// Store new certificate and private key in the key store
store.setKeyEntry( alias, keyPair.getPrivate(), configuration.getPassword(), new X509Certificate[]{cert} );
// Persist the changes in the store to disk.
persist();
}
catch ( CertificateStoreConfigException | IOException | GeneralSecurityException ex )
{
reload(); // reset state of the store.
throw new CertificateStoreConfigException( "Unable to generate new self-signed " + algorithm + " certificate.", ex );
}
// TODO Notify listeners that a new certificate has been created
}
/**
* Returns a new public & private key with the specified algorithm (e.g. DSA, RSA, etc.).
*
* @param algorithm DSA, RSA, etc.
* @param keySize the desired key size. This is an algorithm-specific metric, such as modulus length, specified in number of bits.
* @return a new public & private key with the specified algorithm (e.g. DSA, RSA, etc.).
*/
protected static synchronized KeyPair generateKeyPair( String algorithm, int keySize ) throws GeneralSecurityException
{
final KeyPairGenerator generator;
if ( PROVIDER == null )
{
generator = KeyPairGenerator.getInstance( algorithm );
}
else
{
generator = KeyPairGenerator.getInstance( algorithm, PROVIDER );
}
generator.initialize( keySize, new SecureRandom() );
return generator.generateKeyPair();
}
/**
* Verifies that the subject of the certificate matches the domain of this XMPP service.
*
* @param certificate The certificate to verify (cannot be null)
* @return true when the certificate subject is this domain, otherwise false.
*/
public static boolean isForThisDomain( X509Certificate certificate )
{
final String domainName = XMPPServer.getInstance().getServerInfo().getXMPPDomain();
final List serverIdentities = CertificateManager.getServerIdentities( certificate );
for ( String identity : serverIdentities )
{
if ( DNSUtil.isNameCoveredByPattern( domainName, identity ) )
{
return true;
}
}
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 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 );
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy