/*
 * Written by Dawid Kurzyniec and released to the public domain, as explained
 * at http://creativecommons.org/licenses/publicdomain
 */

package edu.emory.mathcs.util.security;

import java.util.*;
import java.security.*;
import java.io.*;
import java.security.cert.Certificate;
import java.security.cert.*;
import java.security.spec.*;
import java.net.*;
import edu.emory.mathcs.util.security.action.*;



/**
 * Set of methods simplifying manipulation of X.509 certificates and keystores.
 *
 * @author Dawid Kurzyniec
 * @version 1.0
 */

public class CertUtils {

    private static CertificateFactory x509certFact;

    private CertUtils() {}

    /**
     * Returns a default X.509 certificate factory.
     * @return default X.509 certificate factory
     */
    public synchronized static CertificateFactory getX509CertFactory() {
        if (x509certFact == null) {
            try {
                x509certFact = CertificateFactory.getInstance("X.509");
            }
            catch (CertificateException e) {
                throw new RuntimeException("FATAL: X.509 factory not supported");
            }
        }
        return x509certFact;
    }

    /**
     * Creates an empty keystore of the default type.
     *
     * @return newly created keystore
     */
    public static KeyStore createKeystore() {
        try {
            return createKeystore(KeyStore.getDefaultType());
        }
        catch (KeyStoreException e) {
            throw new RuntimeException("FATAL: keystore type \"" +
                                       KeyStore.getDefaultType() + "\" not supported");
        }
    }

    /**
     * Creates an empty keystore of the specified type.
     *
     * @return newly created keystore
     * @throws KeyStoreException if keystore could not be created
     */
    public static KeyStore createKeystore(String type) throws KeyStoreException {
        KeyStore ks = KeyStore.getInstance(type);
        try {
            ks.load(null, null);
            return ks;
        }
        catch (IOException e) {
            throw new RuntimeException("FATAL: can't initialize empty keystore");
        }
        catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("FATAL: can't initialize empty keystore");
        }
        catch (CertificateException e) {
            throw new RuntimeException("FATAL: can't initialize empty keystore");
        }
    }

    /**
     * Load keystore of the default type from the specified file, using the
     * specified password.
     *
     * @param file file to read keystore from
     * @param passwd keystore password
     *
     * @return keystore loaded from the file
     * @throws IOException if there is an I/O or format problem with the
     *         keystore data
     * @throws CertificateException if any of the certificates in the keystore
     *         could not be loaded
     * @throws NoSuchAlgorithmException if the algorithm used to check the
     *         integrity of the keystore cannot be found
     */
    public static KeyStore loadKeystore(File file, char[] passwd)
        throws IOException, CertificateException, NoSuchAlgorithmException
    {
        try {
            return loadKeystore(file, passwd, KeyStore.getDefaultType());
        }
        catch (KeyStoreException e) {
            throw new RuntimeException("FATAL: keystore type \"" +
                                       KeyStore.getDefaultType() + "\" not supported");
        }
    }

    /**
     * Load keystore of the specified type from the specified file, using the
     * specified password.
     *
     * @param file file to read keystore from
     * @param type keystore type
     * @param passwd keystore password
     *
     * @return keystore loaded from the file
     * @throws IOException if there is an I/O or format problem with the
     *         keystore data
     * @throws CertificateException if any of the certificates in the keystore
     *         could not be loaded
     * @throws NoSuchAlgorithmException if the algorithm used to check the
     *         integrity of the keystore cannot be found
     */
    public static KeyStore loadKeystore(File file, char[] passwd, String type)
        throws IOException, KeyStoreException, CertificateException,
               NoSuchAlgorithmException
    {
        FileInputStream fis = new FileInputStream(file);
        KeyStore ks = KeyStore.getInstance(type);
        ks.load(fis, passwd);
        fis.close();
        return ks;
    }

    /**
     * Returns a list of certificates kept in the specified keystore.
     *
     * @param ks the keystore
     * @return list of certificates kept in the keystore
     */
    public static List getKeystoreCerts(KeyStore ks) {
        List list = new ArrayList();
        try {
            Enumeration aliases = ks.aliases();
            while (aliases.hasMoreElements()) {
                String alias = (String) aliases.nextElement();
                if (! (ks.isCertificateEntry(alias)))
                    continue;
                Certificate c = ks.getCertificate(alias);
                list.add(c);
            }
            return list;
        }
        catch (KeyStoreException e) {
            throw new RuntimeException("Keystore not loaded", e);
        }
    }

    /**
     * Generate a collection of trust anchors representing specified
     * certificates.
     *
     * @param certs certificates
     * @return trust anchors representing the certificates
     */
    public static Collection createTrustAnchors(Collection certs) {
        return createTrustAnchors(certs, null);
    }

    /**
     * Generate a collection of trust anchors representing specified
     * certificates, using specified nameConstraints.
     *
     * @param certs certificates
     * @param nameConstraints a byte array containing the ASN.1 DER encoding
     *        of a NameConstraints extension to be used for checking name
     *        constraints.
     * @return trust anchors representing the certificates
     */
    public static Collection createTrustAnchors(Collection certs, byte[] nameConstraints) {
        Set anchors = new HashSet(certs.size());
        for (Iterator i = certs.iterator(); i.hasNext(); ) {
            Certificate cert = (Certificate)i.next();
            if (cert instanceof X509Certificate) {
                anchors.add(new TrustAnchor((X509Certificate)cert, nameConstraints));
            }
        }
        return anchors;
    }

    /**
     * Returns the collection of default JSSE trust anchors. Uses the following
     * truststore search order:<br>
     *
     * 1) system property <code>javax.net.ssl.trustStore</code>, <br>
     * 2) <code>${java.home}/lib/security/jssecacerts</code>,    <br>
     * 3) <code>${java.home}/lib/security/cacerts</code>         <br>
     *
     * @return the collection of default JSSE trust anchors.
     */
    public static Collection getJSSETrustAnchors() {
        String trustStore = (String)AccessController.doPrivileged(
            new GetPropertyAction("javax.net.ssl.trustStore"));
        if (trustStore != null) {
            String trustStorePassStr = (String)AccessController.doPrivileged(
                new GetPropertyAction("javax.net.ssl.trustStorePassword", ""));
            char[] pass = trustStorePassStr.toCharArray();
            KeyStore ks;
            File file = new File(trustStore);
            try {
                if (file.exists()) {
                    ks = loadKeystore(file, pass);
                    return createTrustAnchors(getKeystoreCerts(ks));
                }
            }
            catch (IOException e) {}
            catch (NoSuchAlgorithmException e) {}
            catch (CertificateException e) {}

            return Collections.EMPTY_SET;
        }

        return (Collection)AccessController.doPrivileged(new PrivilegedAction() {
            public Object run() {
                return getDefaultJavaTrustAnchorsPrivileged();
            }
        });
    }

    private static Collection getDefaultJavaTrustAnchorsPrivileged() {
        String javaHome = System.getProperty("java.home");
        File jssecacerts =
            new File(javaHome, "lib/security/jssecacerts".replace('/',File.separatorChar));
        File cacerts =
            new File(javaHome, "lib/security/cacerts".replace('/',File.separatorChar));
        Set trustAnchors = new HashSet();
        KeyStore ks;
        try {
            ks = jssecacerts.exists() ? loadKeystore(jssecacerts, null)
                                      : loadKeystore(cacerts, null);
            return createTrustAnchors(getKeystoreCerts(ks));
        }
        catch (IOException e) {
            throw new RuntimeException(e);
        }
        catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
        catch (CertificateException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Retrieve X.509 certificates represented by the specified trust anchors.
     *
     * @param trustAnchors the collection of trust anchors
     * @return certificates represented by the trust anchors
     */
    public static X509Certificate[] getX509Certs(Collection trustAnchors) {
        List certs = new ArrayList(trustAnchors.size());
        for (Iterator i = trustAnchors.iterator(); i.hasNext(); ) {
            TrustAnchor anchor = (TrustAnchor)i.next();
            certs.add(anchor.getTrustedCert());
        }
        return (X509Certificate[])certs.toArray(new X509Certificate[certs.size()]);
    }

    /**
     * Returns an instance of PKIX certificate path validator.
     * @return an instance of PKIX certificate path validator
     */
    public static CertPathValidator createPKIXValidator() {
        try {
            return CertPathValidator.getInstance("PKIX");
        }
        catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("FATAL: PKIX validation not supported");
        }
    }

    /**
     * Converts specified list of certificates into a certificate path, using
     * default X.509 certificate factory.
     *
     * @param certs certificates to turn into a certificate path
     * @return newly created certificate path
     */
    public static CertPath convertToCertPath(X509Certificate[] certs) {
        try {
            return getX509CertFactory().generateCertPath(Arrays.asList(certs));
        }
        catch (CertificateException e) {
            throw new RuntimeException("FATAL: X509 cert path construction failed");
        }
    }

    /**
     * Decode the X.509 certificate out of its default byte encoding.
     * @param encoded the encoded certificate
     * @return the decoded certificate
     *
     * @throws CertificateException if the supplied parameter is not a valid
     *         encoding of an X.509 certificate
     */
    public static X509Certificate decodeX509Cert(byte[] encoded) throws CertificateException {
        return (X509Certificate)getX509CertFactory().generateCertificate(
            new ByteArrayInputStream(encoded));
    }

    /**
     * Encode the X.509 certificate to its default byte encoding.
     *
     * @param cert the certificate
     * @return the encoded certificate
     */
    public static byte[] encodeX509Cert(X509Certificate cert) {
        try {
            return cert.getEncoded();
        }
        catch (CertificateEncodingException e) {
            throw new RuntimeException("FATAL: X509 cert can't be encoded");
        }
    }

    public static void verifySSLServerHostname(X509Certificate cert, String hostname)
        throws CertificateException
    {
        final String subject = getCN(cert);
        if (!serverHostnameMatches(subject, hostname)) {
            throw new CertificateException("The certificate subject \"" +
               subject + "\" does not match the host name \"" + hostname + "\"");
        }
    }

    public static String getCN(X509Certificate cert) {
        String dn = cert.getSubjectX500Principal().getName("RFC1779");
        String dnl = dn.toLowerCase();
        int beg = dnl.indexOf("cn=");
        if (beg < 0) return null;
        int end = dnl.indexOf(',', beg);
        if (end >= beg) {
            dn = dn.substring(beg+"cn=".length(), end);
        }
        else {
            dn = dn.substring(beg+"cn=".length());
        }
        dn = dn.trim();
        if (dn.startsWith("\"")) dn = dn.substring(1);
        if (dn.endsWith("\"")) dn = dn.substring(0, dn.length()-1);
        return dn;
    }

    private static boolean serverHostnameMatches(final String subject,
                                                 final String hostname)
    {
        if (hostname.equals(subject)) return true;
        return ((Boolean)AccessController.doPrivileged(new PrivilegedAction() {
            public Object run() {
                try {
                    InetAddress addr1 = InetAddress.getByName(hostname);
                    InetAddress addr2 = InetAddress.getByName(subject);
                    String n1 = addr1.getCanonicalHostName();
                    String n2 = addr2.getCanonicalHostName();
                    return (n1.equals(n2)) ? Boolean.TRUE : Boolean.FALSE;
                }
                catch (UnknownHostException e) {
                    return Boolean.FALSE;
                }
            }
        })).booleanValue();
    }
}
