/**
 * $RCSfile$
 * $Revision: $
 * $Date: $
 *
 * Copyright (C) 2005-2008 Jive Software. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jivesoftware.util;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Security;
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.CertificateParsingException;
import java.security.cert.CollectionCertStoreParameters;
import java.security.cert.PKIXBuilderParameters;
import java.security.cert.TrustAnchor;
import java.security.cert.X509CertSelector;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1TaggedObject;
import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.DERObjectIdentifier;
import org.bouncycastle.asn1.DEROutputStream;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.DERTaggedObject;
import org.bouncycastle.asn1.DERUTF8String;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.asn1.x509.X509Extensions;
import org.bouncycastle.asn1.x509.X509Name;
import org.bouncycastle.jce.PKCS10CertificationRequest;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.PEMDecryptorProvider;
import org.bouncycastle.openssl.PEMEncryptedKeyPair;
import org.bouncycastle.openssl.PasswordFinder;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;
import org.bouncycastle.x509.X509V3CertificateGenerator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 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.
 *
 * @author Gaston Dombiak
 */
public class CertificateManager {

	private static final Logger Log = LoggerFactory.getLogger(CertificateManager.class);

    private static final String OTHERNAME_XMPP_OID = "1.3.6.1.5.5.7.8.5";

    private static Pattern cnPattern = Pattern.compile("(?i)(cn=)([^,]*)");
    private static Pattern valuesPattern = Pattern.compile("(?i)(=)([^,]*)");

    private static Provider provider = new BouncyCastleProvider();

    /**
     * The maximum length of lines in certification requests
     */
    private static final int CERT_REQ_LINE_LENGTH = 76;

    private static List<CertificateEventListener> listeners = new CopyOnWriteArrayList<CertificateEventListener>();

    static {
        // Add the BC provider to the list of security providers
        Security.addProvider(provider);
    }

    /**
     * Creates a new X509 certificate using the DSA algorithm. The new certificate together with its private
     * key are stored in the specified key store. However, the key store is not saved to the disk. This means
     * that it is up to the "caller" to save the key store to disk after new certificates have been added
     * to the store.
     *
     * @param ksKeys    key store where the new certificate and private key are going to be stored.
     * @param keyPassword password of the keystore.
     * @param alias     name to use when storing the certificate in the key store.
     * @param issuerDN  Issuer string e.g "O=Grid,OU=OGSA,CN=ACME"
     * @param subjectDN Subject string e.g "O=Grid,OU=OGSA,CN=John Doe"
     * @param domain    domain of the server to store in the subject alternative name extension.
     * @return the new X509 V3 Certificate.
     * @throws GeneralSecurityException
     * @throws IOException
     */
    public static X509Certificate createDSACert(KeyStore ksKeys, String keyPassword, String alias, String issuerDN,
                                                String subjectDN, String domain)
            throws GeneralSecurityException, IOException {
        // Generate public and private keys
        KeyPair keyPair = generateKeyPair("DSA", JiveGlobals.getIntProperty("cert.dsa.keysize", 1024));
        // Create X509 certificate with keys and specified domain
        X509Certificate cert = createX509V3Certificate(keyPair, 60, issuerDN, subjectDN, domain, "SHA1withDSA");
        // Store new certificate and private key in the keystore
        ksKeys.setKeyEntry(alias, keyPair.getPrivate(), keyPassword.toCharArray(), new X509Certificate[]{cert});
        // Notify listeners that a new certificate has been created
        for (CertificateEventListener listener : listeners) {
            try {
                listener.certificateCreated(ksKeys, alias, cert);
            }
            catch (Exception e) {
                Log.error(e.getMessage(), e);
            }
        }
        // Return new certificate
        return cert;
    }

    /**
     * Creates a new X509 certificate using the RSA algorithm. The new certificate together with its private
     * key are stored in the specified key store. However, the key store is not saved to the disk. This means
     * that it is up to the "caller" to save the key store to disk after new certificates have been added
     * to the store.
     *
     * @param ksKeys    key store where the new certificate and private key are going to be stored.
     * @param keyPassword password of the keystore.
     * @param alias     name to use when storing the certificate in the key store.
     * @param issuerDN  Issuer string e.g "O=Grid,OU=OGSA,CN=ACME"
     * @param subjectDN Subject string e.g "O=Grid,OU=OGSA,CN=John Doe"
     * @param domain    domain of the server to store in the subject alternative name extension.
     * @return the new X509 V3 Certificate.
     * @throws GeneralSecurityException
     * @throws IOException
     */
    public static X509Certificate createRSACert(KeyStore ksKeys, String keyPassword, String alias, String issuerDN,
                                                String subjectDN, String domain)
            throws GeneralSecurityException, IOException {
        // Generate public and private keys
        KeyPair keyPair = generateKeyPair("RSA", JiveGlobals.getIntProperty("cert.rsa.keysize", 2048));
        // Create X509 certificate with keys and specified domain
        X509Certificate cert = createX509V3Certificate(keyPair, 60, issuerDN, subjectDN, domain, "SHA1WITHRSAENCRYPTION");
        // Store new certificate and private key in the keystore
        ksKeys.setKeyEntry(alias, keyPair.getPrivate(), keyPassword.toCharArray(), new X509Certificate[]{cert});
        // Notify listeners that a new certificate has been created
        for (CertificateEventListener listener : listeners) {
            try {
                listener.certificateCreated(ksKeys, alias, cert);
            }
            catch (Exception e) {
                Log.error(e.getMessage(), e);
            }
        }
        // Return new certificate
        return cert;
    }

    /**
     * Deletes the specified certificate from the
     *
     * @param ksKeys    key store where the certificate is stored.
     * @param alias     alias of the certificate to delete.
     * @throws GeneralSecurityException
     * @throws IOException
     */
    public static void deleteCertificate(KeyStore ksKeys, String alias) throws GeneralSecurityException, IOException {
        ksKeys.deleteEntry(alias);
        // Notify listeners that a new certificate has been created
        for (CertificateEventListener listener : listeners) {
            try {
                listener.certificateDeleted(ksKeys, alias);
            }
            catch (Exception e) {
                Log.error(e.getMessage(), e);
            }
        }
    }

    /**
     * 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.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<Certificate>();
        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 server as defined in the specified certificate. The
     * identities are defined in the subjectDN of the certificate and it can also be defined in
     * the subjectAltName extensions of type "xmpp". When the extension is being used then the
     * identities defined in the extension are going to be returned. Otherwise, the value stored in
     * the subjectDN is returned.
     *
     * @param x509Certificate the certificate the holds the identities of the remote server.
     * @return the identities of the remote server as defined in the specified certificate.
     */
    public static List<String> getPeerIdentities(X509Certificate x509Certificate) {
        // Look the identity in the subjectAltName extension if available
        List<String> names = getSubjectAlternativeNames(x509Certificate);
        if (names.isEmpty()) {
            String name = x509Certificate.getSubjectDN().getName();
            Matcher matcher = cnPattern.matcher(name);
            // Create an array with the detected identities
            names = new ArrayList<String>();
            while (matcher.find()) {
                names.add(matcher.group(2));
            }
        }
        return names;
    }

    /**
     * Returns the JID representation of an XMPP entity contained as a SubjectAltName extension
     * in the certificate. If none was found then return <tt>null</tt>.
     *
     * @param certificate the certificate presented by the remote entity.
     * @return the JID representation of an XMPP entity contained as a SubjectAltName extension
     *         in the certificate. If none was found then return <tt>null</tt>.
     */
    private static List<String> getSubjectAlternativeNames(X509Certificate certificate) {
        List<String> identities = new ArrayList<String>();
        try {
            Collection<List<?>> altNames = certificate.getSubjectAlternativeNames();
            // Check that the certificate includes the SubjectAltName extension
            if (altNames == null) {
                return Collections.emptyList();
            }
            // Use the type OtherName to search for the certified server name
            for (List<?> item : altNames) {
                Integer type = (Integer) item.get(0);
                if (type == 0) {
                    // Type OtherName found so return the associated value
                    try {
                        // Value is encoded using ASN.1 so decode it to get the server's identity
                        ASN1InputStream decoder = new ASN1InputStream((byte[]) item.get(1));
                        Object object = decoder.readObject();
                        ASN1Sequence otherNameSeq = null;
                        if (object != null && object instanceof ASN1Sequence) {
                        	otherNameSeq = (ASN1Sequence) object;
                        } else {
                        	continue;
                        }
                        // Check the object identifier
                        ASN1ObjectIdentifier objectId = (ASN1ObjectIdentifier) otherNameSeq.getObjectAt(0);
                    	Log.debug("Parsing otherName for subject alternative names: " + objectId.toString() );

                        if ( !OTHERNAME_XMPP_OID.equals(objectId.getId())) {
                            // Not a XMPP otherName
                            Log.debug("Ignoring non-XMPP otherName, " + objectId.getId());
                            continue;
                        }

                        // Get identity string
                        try {
                        	final String identity;
	                        ASN1Encodable o = otherNameSeq.getObjectAt(1);
	                        if (o instanceof DERTaggedObject) {
	                        	ASN1TaggedObject ato = DERTaggedObject.getInstance(o);
	                        	Log.debug("... processing DERTaggedObject: " + ato.toString());
	                        	// TODO: there's bound to be a better way...
	                        	identity = ato.toString().substring(ato.toString().lastIndexOf(']')+1).trim();
	                        } else {
	                        	DERUTF8String derStr = DERUTF8String.getInstance(o);
		                        identity = derStr.getString();
	                        }
	                        if (identity != null && identity.length() > 0) {
	                            // Add the decoded server name to the list of identities
	                            identities.add(identity);
	                        }
	                        decoder.close();
                        } catch (IllegalArgumentException ex) {
                        	// OF-517: othername formats are extensible. If we don't recognize the format, skip it.
                        	Log.debug("Cannot parse altName, likely because of unknown record format.", ex);
                        }
                    }
                    catch (UnsupportedEncodingException e) {
                        // Ignore
                    }
                    catch (IOException e) {
                        // Ignore
                    }
                    catch (Exception e) {
                        Log.error("Error decoding subjectAltName", e);
                    }
                }
                // Other types are not applicable for XMPP, so silently ignore them
            }
        }
        catch (CertificateParsingException e) {
            Log.error("Error parsing SubjectAltName in certificate: " + certificate.getSubjectDN(), e);
        }
        return identities;
    }

    /**
     * Returns true if an RSA certificate was found in the specified keystore for the specified domain.
     *
     * @param ksKeys the keystore that contains the certificates.
     * @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(KeyStore ksKeys, String domain) throws KeyStoreException {
        return isCertificate(ksKeys, domain, "RSA");
    }

    /**
     * Returns true if an DSA certificate was found in the specified keystore for the specified domain.
     *
     * @param ksKeys the keystore that contains the certificates.
     * @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(KeyStore ksKeys, String domain) throws KeyStoreException {
        return isCertificate(ksKeys, 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 the key store.
     *
     * @param ksKeys the keystore 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 specifed configuration was found in the key store.
     * @throws KeyStoreException
     */
    private static boolean isCertificate(KeyStore ksKeys, String domain, String algorithm) throws KeyStoreException {
        boolean result = false;
    	for (Enumeration<String> aliases = ksKeys.aliases(); aliases.hasMoreElements();) {
            X509Certificate certificate = (X509Certificate) ksKeys.getCertificate(aliases.nextElement());
            if ("*".equals(domain)) {
                // Any domain certified by the certificate is accepted
                if (certificate.getPublicKey().getAlgorithm().equals(algorithm)) {
                    result = true;
                }
            }
            else {
                // Only accept certified domains that match the specified domain
                for (String identity : getPeerIdentities(certificate)) {
                    if (identity.endsWith(domain) && certificate.getPublicKey().getAlgorithm().equals(algorithm)) {
                        result = true;
                    }
                }
            }
        }

    	Log.debug("Check for certificate for '{}' using algorithm {} returned: {}", new Object[] { domain, algorithm, result} );
    	return result;
    }

    /**
     * Returns true if the specified certificate is a self-signed certificate.
     *
     * @param keyStore key store that holds the certificate to verify.
     * @param alias alias of the certificate in the key store.
     * @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(KeyStore keyStore, String alias) throws KeyStoreException {
        // Get certificate chain
        java.security.cert.Certificate[] certificateChain = keyStore.getCertificateChain(alias);
        // Verify that the chain is empty or was signed by himself
        return certificateChain == null || certificateChain.length == 1;
    }

    /**
     * Returns true if the specified certificate is a self-signed certificate. If the certificate
     * was not found in the store then a KeyStoreException is returned.
     *
     * @param keyStore key store that holds the certificate to verify.
     * @param certificate the certificate in the key store.
     * @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(KeyStore keyStore, X509Certificate certificate) throws KeyStoreException {
        String alias = keyStore.getCertificateAlias(certificate);
        if (alias == null) {
            throw new KeyStoreException("Certificate not found in store: " + certificate);
        }
        return isSelfSignedCertificate(keyStore, alias);
    }

    /**
     * Returns true if the specified certificate is ready to be signed by a Certificate Authority. Self-signed
     * certificates need to get their issuer information entered to be able to generate a Certificate
     * Signing Request (CSR).
     *
     * @param keyStore key store that holds the certificate to verify.
     * @param alias alias of the certificate in the key store.
     * @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(KeyStore keyStore, String alias) throws KeyStoreException {
        // Verify that this is a self-signed certificate
        if (!isSelfSignedCertificate(keyStore, alias)) {
            return false;
        }
        // Verify that the issuer information has been entered
        X509Certificate certificate = (X509Certificate) keyStore.getCertificate(alias);
        Matcher matcher = valuesPattern.matcher(certificate.getIssuerDN().toString());
        return matcher.find() && matcher.find();
    }

    /**
     * Creates and returns the content of a new singing request for the specified certificate. Signing
     * 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, String, String, java.io.InputStream, boolean, boolean)}
     * to import the CA reply.
     *
     * @param cert the certificate to create a signing request.
     * @param privKey the private key of the certificate.
     * @return the content of a new singing request for the specified certificate.
     * @throws Exception
     */
    public static String createSigningRequest(X509Certificate cert, PrivateKey privKey) throws Exception {
        StringBuilder sb = new StringBuilder();

        String subject = cert.getSubjectDN().getName();
        X509Name xname = new X509Name(subject);

        PublicKey pubKey = cert.getPublicKey();

        String signatureAlgorithm = "DSA".equals(pubKey.getAlgorithm()) ? "SHA1withDSA" : "SHA1WITHRSAENCRYPTION";

        PKCS10CertificationRequest csr =
                new PKCS10CertificationRequest(signatureAlgorithm, xname, pubKey, null, privKey);

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        DEROutputStream deros = new DEROutputStream(baos);
        deros.writeObject(csr.toASN1Primitive());
        String sTmp = new String(org.bouncycastle.util.encoders.Base64.encode(baos.toByteArray()));

        // Header
        sb.append("-----BEGIN NEW CERTIFICATE REQUEST-----\n");

        // Add signing request content (base 64 encoded)
        for (int iCnt = 0; iCnt < sTmp.length(); iCnt += CERT_REQ_LINE_LENGTH) {
            int iLineLength;

            if ((iCnt + CERT_REQ_LINE_LENGTH) > sTmp.length()) {
                iLineLength = sTmp.length() - iCnt;
            } else {
                iLineLength = CERT_REQ_LINE_LENGTH;
            }

            sb.append(sTmp.substring(iCnt, iCnt + iLineLength)).append("\n");
        }

        // Footer
        sb.append("-----END NEW CERTIFICATE REQUEST-----\n");
        return sb.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). When <tt>trustCACerts</tt>
     * is set to <tt>true</tt> then certificates present in the truststore file will be used to verify the
     * identity of the entity signing the certificate. In case the reply is composed of more than one
     * certificate then you can also specify if you want to verify that the root certificate in the chain
     * can be trusted.
     *
     * @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.
     * @param trustCACerts true if certificates present in the truststore file will be used to verify the
     *        identity of the entity signing the certificate.
     * @param validateRoot true if you want to verify that the root certificate in the chain can be trusted
     *        based on the truststore.
     * @return true if the CA reply was successfully processed.
     * @throws Exception
     */
    public static boolean installReply(KeyStore keyStore, KeyStore trustStore, String keyPassword, String alias, InputStream inputStream, boolean trustCACerts,
                                       boolean validateRoot) 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.toCharArray());
        // Load certificates found in the PEM input stream
        List<X509Certificate> certs = new ArrayList<X509Certificate>();
        for (Certificate cert : CertificateFactory.getInstance("X509").generateCertificates(inputStream)) {
            certs.add((X509Certificate) cert);
        }
        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.get(0), trustCACerts);
        } else {
            // Reply has a chain of certificates
            newCerts = validateReply(keyStore, trustStore, alias, null, certs, trustCACerts, validateRoot);
        }
        if (newCerts != null) {
            keyStore.setKeyEntry(alias, privKey, keyPassword.toCharArray(),
                    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;
        } else {
            return false;
        }
    }

    /**
     * Imports one certificate into a truststore.
     *
     * This method will fail when more than one certificate is being provided.
     *
     * @param trustStore store where certificates are stored.
     * @param alias the name (key) under which the certificate is to be stored in the store.
     * @param inputStream a stream containing the certificate.
     */
    public static void installCertsInTrustStore(KeyStore trustStore, String alias, InputStream inputStream) throws Exception
    {
        // Input validation
        if (trustStore == null) {
            throw new IllegalArgumentException("Argument 'trustStore' cannot be null.");
        }
        if (alias == null || alias.trim().isEmpty()) {
            throw new IllegalArgumentException("Argument 'alias' cannot be null or an empty String.");
        }
        if (inputStream == null) {
            throw new IllegalArgumentException("Argument 'inputStream' cannot be null.");
        }
        alias = alias.trim();

        // Check that there is a certificate for the specified alias
        if (trustStore.containsAlias(alias)) {
            throw new IllegalArgumentException("Certificate already exists for alias: " + alias);
        }

        // Load certificate found in the PEM input stream
        final Collection<? extends Certificate> certificates = CertificateFactory.getInstance("X509").generateCertificates(inputStream);
        if (certificates.isEmpty()) {
            throw new Exception("No certificate was found in the input.");
        }
        if (certificates.size() != 1) {
            throw new Exception("More than one certificate was found in the input.");
        }

        final X509Certificate certificate = (X509Certificate) certificates.iterator().next();

        trustStore.setCertificateEntry(alias, certificate);

        // Notify listeners that a new certificate has been added.
        for (CertificateEventListener listener : listeners) {
            try {
                listener.certificateCreated(trustStore, alias, certificate);
            } catch (Throwable e) {
                Log.warn("An exception occurred during the invocation of a CertificateEventListener.", e);
            }
        }
    }

    /**
     * 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.
     * @param trustCACerts true if certificates present in the truststore file will be used to verify the
     *        identity of the entity signing the certificate.
     * @param validateRoot true if you want to verify that the root certificate in the chain can be trusted
     *        based on the truststore.
     * @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,
                                      boolean trustCACerts, boolean validateRoot) 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;
        }

        PEMParser pemParser = new PEMParser(new InputStreamReader(pkInputStream));
		Object object = pemParser.readObject();
		PEMDecryptorProvider decProv = new JcePEMDecryptorProviderBuilder().build(passPhrase.toCharArray());
		JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC");

		KeyPair kp;

		if (object instanceof PEMEncryptedKeyPair) {
			Log.debug("Encrypted key - we will use provided password");
			kp = converter.getKeyPair(((PEMEncryptedKeyPair) object).decryptKeyPair(decProv));
		} else {
			Log.debug("Unencrypted key - no password needed");
			kp = converter.getKeyPair((PEMKeyPair) object);
		}

        PrivateKey privKey = kp.getPrivate();

        // Load certificates found in the PEM input stream
        List<X509Certificate> certs = new ArrayList<X509Certificate>();
        for (Certificate cert : CertificateFactory.getInstance("X509").generateCertificates(inputStream)) {
            certs.add((X509Certificate) cert);
        }
        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.get(0), trustCACerts);
        } else {
            // Reply has a chain of certificates
            newCerts = validateReply(keyStore, trustStore, alias, certificate, certs, trustCACerts, validateRoot);
        }
        if (newCerts != null) {
            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, certs.get(0));
                    if (newCerts.size() > 1) {
                        listener.certificateSigned(keyStore, alias, newCerts);
                    }
                }
                catch (Exception e) {
                    Log.error(e.getMessage(), e);
                }
            }

            return true;
        } else {
            return false;
        }
    }

    /**
     * Registers a listener to receive events.
     *
     * @param listener the listener.
     */
    public static void addListener(CertificateEventListener listener) {
        if (listener == null) {
            throw new NullPointerException();
        }
        listeners.add(listener);
    }

    /**
     * Unregisters a listener to receive events.
     *
     * @param listener the listener.
     */
    public static void removeListener(CertificateEventListener listener) {
        listeners.remove(listener);
    }

    private static List<X509Certificate> establishCertChain(KeyStore keyStore, KeyStore trustStore,
                                                                  X509Certificate certificate,
                                                                  X509Certificate certReply, boolean trustCACerts)
            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<Principal, List<X509Certificate>> knownCerts = new Hashtable<Principal, List<X509Certificate>>();
        if (keyStore.size() > 0) {
            knownCerts.putAll(getCertsByIssuer(keyStore));
        }
        if (trustCACerts && trustStore.size() > 0) {
            knownCerts.putAll(getCertsByIssuer(trustStore));
        }
        LinkedList<X509Certificate> answer = new LinkedList<X509Certificate>();
        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<Principal, 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);
        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
     */
    private static Map<Principal, List<X509Certificate>> getCertsByIssuer(KeyStore ks)
            throws Exception {
        Map<Principal, List<X509Certificate>> answer = new HashMap<Principal, List<X509Certificate>>();
        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<X509Certificate>();
                    vec.add(cert);
                }
                else {
                    if (!vec.contains(cert)) {
                        vec.add(cert);
                    }
                }
                answer.put(subjectDN, vec);
            }
        }
        return answer;
    }

    /**
     * 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 replyCerts the chain provided in the reply
     */
    private static List<X509Certificate> validateReply(KeyStore keyStore, KeyStore trustStore, String alias,
                                                             X509Certificate userCert, List<X509Certificate> replyCerts,
                                                             boolean trustCACerts, boolean verifyRoot)
            throws Exception {
        // 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());
            }
        }

        if (!verifyRoot) {
            return replyCerts;
        }

        // do we trust the (root) cert at the top?
        X509Certificate topCert = replyCerts.get(replyCerts.size() - 1);
        boolean foundInKeyStore = keyStore.getCertificateAlias(topCert) != null;
        boolean foundInCAStore = trustCACerts && (trustStore.getCertificateAlias(topCert) != null);
        if (!foundInKeyStore && !foundInCAStore) {
            boolean verified = false;
            X509Certificate rootCert = null;
            if (trustCACerts) {
                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.
     *
     * @param kp           KeyPair that keeps the public and private keys for the new certificate.
     * @param months       time to live
     * @param issuerDN     Issuer string e.g "O=Grid,OU=OGSA,CN=ACME"
     * @param subjectDN    Subject string e.g "O=Grid,OU=OGSA,CN=John Doe"
     * @param domain       Domain of the server.
     * @param signAlgoritm Signature algorithm. This can be either a name or an OID.
     * @return X509 V3 Certificate
     * @throws GeneralSecurityException
     * @throws IOException
     */
    private static synchronized X509Certificate createX509V3Certificate(KeyPair kp, int months, String issuerDN,
                                                                        String subjectDN, String domain,
                                                                        String signAlgoritm)
            throws GeneralSecurityException, IOException {
        PublicKey pubKey = kp.getPublic();
        PrivateKey privKey = kp.getPrivate();

        byte[] serno = new byte[8];
        SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
        random.setSeed((new Date().getTime()));
        random.nextBytes(serno);
        BigInteger serial = (new java.math.BigInteger(serno)).abs();

        X509V3CertificateGenerator certGenerator = new X509V3CertificateGenerator();
        certGenerator.reset();

        certGenerator.setSerialNumber(serial);
        certGenerator.setIssuerDN(new X509Name(issuerDN));
        certGenerator.setNotBefore(new Date(System.currentTimeMillis()));
        certGenerator.setNotAfter(
                new Date(System.currentTimeMillis() + months * (1000L * 60 * 60 * 24 * 30)));
        certGenerator.setSubjectDN(new X509Name(subjectDN));
        certGenerator.setPublicKey(pubKey);
        certGenerator.setSignatureAlgorithm(signAlgoritm);

        // Generate the subject alternative name
        boolean critical = subjectDN == null || "".equals(subjectDN.trim());
        ASN1Sequence othernameSequence = new DERSequence(new ASN1Encodable[]{
                new DERObjectIdentifier("1.3.6.1.5.5.7.8.5"), new DERTaggedObject(true, 0, new DERUTF8String(domain))});
        GeneralName othernameGN = new GeneralName(GeneralName.otherName, othernameSequence);
        GeneralNames subjectAltNames = new GeneralNames(new GeneralName[]{othernameGN});
        // Add subject alternative name extension
        certGenerator.addExtension(X509Extensions.SubjectAlternativeName, critical, subjectAltNames);

        X509Certificate cert =
                certGenerator.generateX509Certificate(privKey, "BC", new SecureRandom());
        cert.checkValidity(new Date());
        cert.verify(pubKey);

        return cert;
    }

    /**
     * Returns a new public & private key with the specified algorithm (e.g. DSA, RSA, etc.).
     *
     * @param algorithm DSA, RSA, etc.
     * @param keysize   the keysize. 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.).
     * @throws GeneralSecurityException
     */
    private static KeyPair generateKeyPair(String algorithm, int keysize) throws GeneralSecurityException {
        KeyPairGenerator generator;
        if (provider == null) {
            generator = KeyPairGenerator.getInstance(algorithm);
        } else {
            generator = KeyPairGenerator.getInstance(algorithm, provider);
        }
        generator.initialize(keysize, new SecureRandom());
        return generator.generateKeyPair();
    }
}
