/*
 * Copyright 1999-2010 University of Chicago
 *
 * 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.globus.gsi.bc;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.Security;
import java.security.cert.CertStore;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.X509CertSelector;
import java.security.cert.X509Certificate;
import java.util.Collection;

import javax.naming.InvalidNameException;
import javax.naming.ldap.LdapName;
import javax.security.auth.x500.X500Principal;

import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1OctetString;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.ASN1Set;
import org.bouncycastle.asn1.ASN1String;
import org.bouncycastle.asn1.DERBitString;
import org.bouncycastle.asn1.DERObjectIdentifier;
import org.bouncycastle.asn1.DEROutputStream;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.asn1.x509.BasicConstraints;
import org.bouncycastle.asn1.x509.TBSCertificateStructure;
import org.bouncycastle.asn1.x509.X509Extension;
import org.bouncycastle.asn1.x509.X509Extensions;
import org.bouncycastle.asn1.x509.X509Name;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.globus.gsi.GSIConstants;
import org.globus.gsi.TrustedCertificates;
import org.globus.gsi.TrustedCertificatesUtil;
import org.globus.gsi.proxy.ext.ProxyCertInfo;
import org.globus.gsi.proxy.ext.ProxyPolicy;
import org.globus.gsi.util.ProxyCertificateUtil;
import org.globus.util.I18n;

// COMMENT: BCB: removed methods createCertificateType(...) that took a TBSCertificateStructure as parameter
/**
 * A collection of various utility functions.
 */
public class BouncyCastleUtil {

    static {
	Security.addProvider(new BouncyCastleProvider());
    }

    private static I18n i18n =
        I18n.getI18n("org.globus.gsi.errors",
                     BouncyCastleUtil.class.getClassLoader());

    /**
     * Converts given <code>DERObject</code> into
     * a DER-encoded byte array.
     *
     * @param obj DERObject to convert.
     * @return the DER-encoded byte array
     * @exception IOException if conversion fails
     */
    public static byte[] toByteArray(ASN1Primitive obj)
	throws IOException {
	ByteArrayOutputStream bout = new ByteArrayOutputStream();
	DEROutputStream der = new DEROutputStream(bout);
	der.writeObject(obj);
	return bout.toByteArray();
    }

    /**
     * Converts the DER-encoded byte array into a
     * <code>DERObject</code>.
     *
     * @param data the DER-encoded byte array to convert.
     * @return the DERObject.
     * @exception IOException if conversion fails
     */
    public static ASN1Primitive toASN1Primitive(byte[] data)
	throws IOException {
        ByteArrayInputStream inStream = new ByteArrayInputStream(data);
        ASN1InputStream derInputStream = new ASN1InputStream(inStream);
        return derInputStream.readObject();
    }



    /**
     * Replicates a given <code>DERObject</code>.
     *
     * @param obj the DERObject to replicate.
     * @return a copy of the DERObject.
     * @exception IOException if replication fails
     */
    public static ASN1Primitive duplicate(ASN1Primitive obj)
	throws IOException {
	return toASN1Primitive(toByteArray(obj));
    }

    /**
     * Extracts the TBS certificate from the given certificate.
     *
     * @param cert the X.509 certificate to extract the TBS certificate from.
     * @return the TBS certificate
     * @exception IOException if extraction fails.
     * @exception CertificateEncodingException if extraction fails.
     */
    public static TBSCertificateStructure getTBSCertificateStructure(X509Certificate cert)
	throws CertificateEncodingException, IOException {
	ASN1Primitive obj = BouncyCastleUtil.toASN1Primitive(cert.getTBSCertificate());
	return TBSCertificateStructure.getInstance(obj);
    }

    /**
     * Extracts the value of a certificate extension.
     *
     * @param ext the certificate extension to extract the value from.
     * @exception IOException if extraction fails.
     */
    public static ASN1Primitive getExtensionObject(X509Extension ext)
	throws IOException {
	return toASN1Primitive(ext.getValue().getOctets());
    }

    /**
     * Returns certificate type of the given certificate.
     * Please see {@link #getCertificateType(TBSCertificateStructure,
     * TrustedCertificates) getCertificateType} for details for
     * determining the certificate type.
     *
     * @param cert the certificate to get the type of.
     * @param trustedCerts the trusted certificates to double check the
     *                     {@link GSIConstants#EEC GSIConstants.EEC}
     *                     certificate against.
     * @return the certificate type as determined by
     *             {@link #getCertificateType(TBSCertificateStructure,
     *              TrustedCertificates) getCertificateType}.
     * @exception CertificateException if something goes wrong.
     * @deprecated
     */
    public static GSIConstants.CertificateType getCertificateType(X509Certificate cert,
					 TrustedCertificates trustedCerts)
	throws CertificateException {
        try {
            return getCertificateType(cert, TrustedCertificatesUtil.createCertStore(trustedCerts));
	} catch (Exception e) {
	    throw new CertificateException("", e);
	}
    }

    /**
     * Returns the certificate type of the given certificate.
     * Please see {@link #getCertificateType(TBSCertificateStructure,
     * TrustedCertificates) getCertificateType} for details for
     * determining the certificate type.
     *
     * @param cert the certificate to get the type of.
     * @param trustedCerts the trusted certificates to double check the
     *                     {@link GSIConstants#EEC GSIConstants.EEC}
     *                     certificate against.
     * @return the certificate type as determined by
     *             {@link #getCertificateType(TBSCertificateStructure,
     *              TrustedCertificates) getCertificateType}.
     * @exception CertificateException if something goes wrong.
     */
    public static GSIConstants.CertificateType getCertificateType(X509Certificate cert, CertStore trustedCerts)
    throws CertificateException {
        try {
            TBSCertificateStructure crt = getTBSCertificateStructure(cert);
            GSIConstants.CertificateType type = getCertificateType(crt);

            // check subject of the cert in trusted cert list
            // to make sure the cert is not a ca cert
            if (type == GSIConstants.CertificateType.EEC) {
                X509CertSelector selector = new X509CertSelector();
                selector.setSubject(cert.getSubjectX500Principal());
                Collection c = trustedCerts.getCertificates(selector);
                if (c != null && c.size() > 0) {
                    type = GSIConstants.CertificateType.CA;
                }
            }
            return type;
        } catch (Exception e) {
            // but this should not happen
            throw new CertificateException("", e);
        }
    }

    /**
     * Returns certificate type of the given certificate.
     * Please see {@link #getCertificateType(TBSCertificateStructure)
     * getCertificateType} for details for determining the certificate type.
     *
     * @param cert the certificate to get the type of.
     * @return the certificate type as determined by
     *             {@link #getCertificateType(TBSCertificateStructure)
     *              getCertificateType}.
     * @exception CertificateException if something goes wrong.
     */
    public static GSIConstants.CertificateType getCertificateType(X509Certificate cert)
    throws CertificateException {
    try {
        TBSCertificateStructure crt = getTBSCertificateStructure(cert);
        return getCertificateType(crt);
    } catch (IOException e) {
        // but this should not happen
        throw new CertificateException("", e);
    }
    }

	public static GSIConstants.CertificateType getCertificateType(TBSCertificateStructure crt, TrustedCertificates trustedCerts)
			throws CertificateException, IOException {
		GSIConstants.CertificateType type = getCertificateType(crt);

		// check subject of the cert in trusted cert list
		// to make sure the cert is not a ca cert
		if (type == GSIConstants.CertificateType.EEC) {
			if (trustedCerts == null) {
				trustedCerts = TrustedCertificates.getDefaultTrustedCertificates();
			}
			if (trustedCerts != null && trustedCerts.getCertificate(crt.getSubject().toString()) != null) {
				type = GSIConstants.CertificateType.CA;
			}
		}

		return type;
	}

    /**
     * Returns certificate type of the given TBS certificate. <BR>
     * The certificate type is {@link GSIConstants#CA GSIConstants.CA}
     * <B>only</B> if the certificate contains a
     * BasicConstraints extension and it is marked as CA.<BR>
     * A certificate is a GSI-2 proxy when the subject DN of the certificate
     * ends with <I>"CN=proxy"</I> (certificate type {@link
     * GSIConstants#GSI_2_PROXY GSIConstants.GSI_2_PROXY}) or
     * <I>"CN=limited proxy"</I> (certificate type {@link
     * GSIConstants#GSI_2_LIMITED_PROXY GSIConstants.LIMITED_PROXY}) component
     * and the issuer DN of the certificate matches the subject DN without
     * the last proxy <I>CN</I> component.<BR>
     * A certificate is a GSI-3 proxy when the subject DN of the certificate
     * ends with a <I>CN</I> component, the issuer DN of the certificate
     * matches the subject DN without the last <I>CN</I> component and
     * the certificate contains {@link ProxyCertInfo ProxyCertInfo} critical
     * extension.
     * The certificate type is {@link GSIConstants#GSI_3_IMPERSONATION_PROXY
     * GSIConstants.GSI_3_IMPERSONATION_PROXY} if the policy language of
     * the {@link ProxyCertInfo ProxyCertInfo} extension is set to
     * {@link ProxyPolicy#IMPERSONATION ProxyPolicy.IMPERSONATION} OID.
     * The certificate type is {@link GSIConstants#GSI_3_LIMITED_PROXY
     * GSIConstants.GSI_3_LIMITED_PROXY} if the policy language of
     * the {@link ProxyCertInfo ProxyCertInfo} extension is set to
     * {@link ProxyPolicy#LIMITED ProxyPolicy.LIMITED} OID.
     * The certificate type is {@link GSIConstants#GSI_3_INDEPENDENT_PROXY
     * GSIConstants.GSI_3_INDEPENDENT_PROXY} if the policy language of
     * the {@link ProxyCertInfo ProxyCertInfo} extension is set to
     * {@link ProxyPolicy#INDEPENDENT ProxyPolicy.INDEPENDENT} OID.
     * The certificate type is {@link GSIConstants#GSI_3_RESTRICTED_PROXY
     * GSIConstants.GSI_3_RESTRICTED_PROXY} if the policy language of
     * the {@link ProxyCertInfo ProxyCertInfo} extension is set to
     * any other OID then the above.<BR>
     * The certificate type is {@link GSIConstants#EEC GSIConstants.EEC}
     * if the certificate is not a CA certificate or a GSI-2 or GSI-3 proxy.
     *
     * @param crt the TBS certificate to get the type of.
     * @return the certificate type. The certificate type is determined
     *         by rules described above.
     * @exception IOException if something goes wrong.
     * @exception CertificateException for proxy certificates, if
     *            the issuer DN of the certificate does not match
     *            the subject DN of the certificate without the
     *            last <I>CN</I> component. Also, for GSI-3 proxies
     *            when the <code>ProxyCertInfo</code> extension is
     *            not marked as critical.
     */
    private static GSIConstants.CertificateType getCertificateType(TBSCertificateStructure crt)
	throws CertificateException, IOException {
	X509Extensions extensions = crt.getExtensions();
	X509Extension ext = null;

	if (extensions != null) {
	    ext = extensions.getExtension(X509Extension.basicConstraints);
	    if (ext != null) {
		BasicConstraints basicExt = BasicConstraints.getInstance(ext);
		if (basicExt.isCA()) {
		    return GSIConstants.CertificateType.CA;
		}
	    }
	}

	GSIConstants.CertificateType type = GSIConstants.CertificateType.EEC;

	// does not handle multiple AVAs
	X500Name subject = crt.getSubject();

	ASN1Set entry = X509NameHelper.getLastNameEntry(subject);
	ASN1Sequence ava = (ASN1Sequence)entry.getObjectAt(0);
	if (BCStyle.CN.equals(ava.getObjectAt(0))) {
	    String value = ((ASN1String)ava.getObjectAt(1)).getString();
	    if (value.equalsIgnoreCase("proxy")) {
		type = GSIConstants.CertificateType.GSI_2_PROXY;
	    } else if (value.equalsIgnoreCase("limited proxy")) {
		type = GSIConstants.CertificateType.GSI_2_LIMITED_PROXY;
	    } else if (extensions != null) {
                boolean gsi4 = true;
                // GSI_4
		ext = extensions.getExtension(ProxyCertInfo.OID);
                if (ext == null) {
                    // GSI_3
                    ext = extensions.getExtension(ProxyCertInfo.OLD_OID);
                    gsi4 = false;
                }
		if (ext != null) {
		    if (ext.isCritical()) {
			ProxyCertInfo proxyCertExt = getProxyCertInfo(ext);
                        ProxyPolicy proxyPolicy =
                            proxyCertExt.getProxyPolicy();
                        DERObjectIdentifier oid =
                            proxyPolicy.getPolicyLanguage();
			if (ProxyPolicy.IMPERSONATION.equals(oid)) {
                            if (gsi4) {
                                type = GSIConstants.CertificateType.GSI_4_IMPERSONATION_PROXY;
                            } else {
                                type = GSIConstants.CertificateType.GSI_3_IMPERSONATION_PROXY;
                            }
			} else if (ProxyPolicy.INDEPENDENT.equals(oid)) {
                            if (gsi4) {
                                type = GSIConstants.CertificateType.GSI_4_INDEPENDENT_PROXY;
                            } else {
                                type = GSIConstants.CertificateType.GSI_3_INDEPENDENT_PROXY;
                            }
			} else if (ProxyPolicy.LIMITED.equals(oid)) {
                            if (gsi4) {
                                type = GSIConstants.CertificateType.GSI_4_LIMITED_PROXY;
                            } else {
                                type = GSIConstants.CertificateType.GSI_3_LIMITED_PROXY;
                            }
			} else {
                            if (gsi4) {
                                type = GSIConstants.CertificateType.GSI_4_RESTRICTED_PROXY;
                            } else {
                                type = GSIConstants.CertificateType.GSI_3_RESTRICTED_PROXY;
                            }
			}

		    } else {
                        String err = i18n.getMessage("proxyCertCritical");
			throw new CertificateException(err);
		    }
		}
	    }

	    if (ProxyCertificateUtil.isProxy(type)) {
		X509NameHelper iss = new X509NameHelper(crt.getIssuer());
		iss.add((ASN1Set)BouncyCastleUtil.duplicate(entry));
		X509Name issuer = iss.getAsName();
		if (!issuer.equals(X509Name.getInstance(subject))) {
                    String err = i18n.getMessage("proxyDNErr");
		    throw new CertificateException(err);
		}
	    }
	}

	return type;
    }

    /**
     * Gets a boolean array representing bits of the KeyUsage extension.
     *
     * @see java.security.cert.X509Certificate#getKeyUsage
     * @exception IOException if failed to extract the KeyUsage extension value.
     */
    public static boolean[] getKeyUsage(X509Extension ext)
	throws IOException {
	DERBitString bits = (DERBitString)getExtensionObject(ext);

	// copied from X509CertificateObject
	byte [] bytes = bits.getBytes();
	int length = (bytes.length * 8) - bits.getPadBits();

	boolean[]  keyUsage = new boolean[(length < 9) ? 9 : length];

	for (int i = 0; i != length; i++) {
	    keyUsage[i] = (bytes[i / 8] & (0x80 >>> (i % 8))) != 0;
	}

	return keyUsage;
    }

    /**
     * Creates a <code>ProxyCertInfo</code> object from given
     * extension.
     *
     * @param ext the extension.
     * @return the <code>ProxyCertInfo</code> object.
     * @exception IOException if something fails.
     */
    public static ProxyCertInfo getProxyCertInfo(X509Extension ext)
	throws IOException {
	return ProxyCertInfo.getInstance(BouncyCastleUtil.getExtensionObject(ext));
    }

    /**
     * Returns the subject DN of the given certificate in the Globus format.
     *
     * @param cert the certificate to get the subject of. The certificate
     *             must be of <code>X509CertificateObject</code> type.
     * @return the subject DN of the certificate in the Globus format.
     */
    public static String getIdentity(X509Certificate cert) {
    	if (cert == null) {
    		return null;
    	}

    	String subjectDN = cert.getSubjectX500Principal().getName(X500Principal.RFC2253);
    	X509Name name = new X509Name(true, subjectDN);
	    return X509NameHelper.toString(name);
    }

    public static String getIdentityPrefix(X509Certificate cert) {
    	if (cert == null) {
    		return null;
    	}

    	String subjectDN = cert.getSubjectX500Principal().getName(X500Principal.RFC2253);
    	LdapName ldapname = null;
		try {
			ldapname = new LdapName(subjectDN);
			ldapname.remove(ldapname.size() - 1);
		} catch (InvalidNameException e) {
			return null;
		}
    	X509Name name = new X509Name(true, ldapname.toString());
	    return X509NameHelper.toString(name);
    }

    /**
     * Finds the identity certificate in the given chain and
     * returns the subject DN of that certificate in the Globus format.
     *
     * @param chain the certificate chain to find the identity
     *              certificate in. The certificates must be
     *              of <code>X509CertificateObject</code> type.
     * @return the subject DN of the identity certificate in
     *         the Globus format.
     * @exception CertificateException if something goes wrong.
     */
    public static String getIdentity(X509Certificate [] chain)
	throws CertificateException {
	return getIdentity(getIdentityCertificate(chain));
    }

    /**
     * Finds the identity certificate in the given chain.
     * The identity certificate is the first certificate in the
     * chain that is not an impersonation proxy (full or limited)
     *
     * @param chain the certificate chain to find the identity
     *              certificate in.
     * @return the identity certificate.
     * @exception CertificateException if something goes wrong.
     */
    public static X509Certificate getIdentityCertificate(X509Certificate [] chain)
	throws CertificateException {
	if (chain == null) {
	    throw new IllegalArgumentException(i18n.getMessage("certChainNull"));
	}
	GSIConstants.CertificateType certType;
	for (int i=0;i<chain.length;i++) {
	    certType = getCertificateType(chain[i]);
	    if (!ProxyCertificateUtil.isImpersonationProxy(certType)) {
		return chain[i];
	    }
	}
	return null;
    }

    /**
     * Retrieves the actual value of the X.509 extension.
     *
     * @param certExtValue the DER-encoded OCTET string value of the extension.
     * @return the decoded/actual value of the extension (the octets).
     */
    public static byte[] getExtensionValue(byte [] certExtValue)
	throws IOException {
	ByteArrayInputStream inStream = new ByteArrayInputStream(certExtValue);
	ASN1InputStream derInputStream = new ASN1InputStream(inStream);
        ASN1Primitive object = derInputStream.readObject();
	if (object instanceof ASN1OctetString) {
	    return ((ASN1OctetString)object).getOctets();
	} else {
	    throw new IOException(i18n.getMessage("octectExp"));
	}
    }

    /**
     * Returns the actual value of the extension.
     *
     * @param cert the certificate that contains the extensions to retrieve.
     * @param oid the oid of the extension to retrieve.
     * @return the actual value of the extension (not octet string encoded)
     * @exception IOException if decoding the extension fails.
     */
    public static byte[] getExtensionValue(X509Certificate cert, String oid)
    throws IOException {
    if (cert == null) {
        throw new IllegalArgumentException(i18n.getMessage("certNull"));
    }
    if (oid == null) {
        throw new IllegalArgumentException(i18n.getMessage("oidNull"));
    }

    byte [] value = cert.getExtensionValue(oid);
    if (value == null) {
        return null;
    }

    return getExtensionValue(value);
    }

    public static int getProxyPathConstraint(X509Certificate cert)
            throws IOException, CertificateEncodingException {

        TBSCertificateStructure crt = getTBSCertificateStructure(cert);
        return getProxyPathConstraint(crt);
    }


    public static int getProxyPathConstraint(TBSCertificateStructure crt)
        throws IOException {

        ProxyCertInfo proxyCertExt = getProxyCertInfo(crt);
        return (proxyCertExt != null) ? proxyCertExt.getPathLenConstraint() :
            -1;
    }

    public static ProxyCertInfo getProxyCertInfo(TBSCertificateStructure crt)
	throws IOException {

	X509Extensions extensions = crt.getExtensions();
	if (extensions == null) {
	    return null;
	}
	X509Extension ext =
	    extensions.getExtension(ProxyCertInfo.OID);
        if (ext == null) {
            ext = extensions.getExtension(ProxyCertInfo.OLD_OID);
        }
	return (ext != null) ? BouncyCastleUtil.getProxyCertInfo(ext) : null;
    }

}
