/*
 *
 * This file is part of the iText (R) project.
    Copyright (c) 1998-2017 iText Group NV
 * Authors: Bruno Lowagie, Paulo Soares, et al.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License version 3
 * as published by the Free Software Foundation with the addition of the
 * following permission added to Section 15 as permitted in Section 7(a):
 * FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
 * ITEXT GROUP. ITEXT GROUP DISCLAIMS THE WARRANTY OF NON INFRINGEMENT
 * OF THIRD PARTY RIGHTS
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Affero General Public License for more details.
 * You should have received a copy of the GNU Affero General Public License
 * along with this program; if not, see http://www.gnu.org/licenses or write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA, 02110-1301 USA, or download the license from the following URL:
 * http://itextpdf.com/terms-of-use/
 *
 * The interactive user interfaces in modified source and object code versions
 * of this program must display Appropriate Legal Notices, as required under
 * Section 5 of the GNU Affero General Public License.
 *
 * In accordance with Section 7(b) of the GNU Affero General Public License,
 * a covered work must retain the producer line in every PDF that is created
 * or manipulated using iText.
 *
 * You can be released from the requirements of the license by purchasing
 * a commercial license. Buying such a license is mandatory as soon as you
 * develop commercial activities involving the iText software without
 * disclosing the source code of your own applications.
 * These activities include: offering paid services to customers as an ASP,
 * serving PDFs on the fly in a web application, shipping iText with a closed
 * source product.
 *
 * For more information, please contact iText Software Corp. at this
 * address: sales@itextpdf.com
 */
package com.itextpdf.text.pdf.security;

import java.security.KeyStore;
import java.security.cert.CRL;
import java.security.cert.Certificate;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Enumeration;
import java.util.GregorianCalendar;
import java.util.List;

import org.bouncycastle.cert.ocsp.BasicOCSPResp;
import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder;
import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder;
import org.bouncycastle.tsp.TimeStampToken;

/**
 * This class consists of some methods that allow you to verify certificates.
 */
public class CertificateVerification {

    /**
     * Verifies a single certificate.
     * @param cert the certificate to verify
     * @param crls the certificate revocation list or <CODE>null</CODE>
     * @param calendar the date or <CODE>null</CODE> for the current date
     * @return a <CODE>String</CODE> with the error description or <CODE>null</CODE>
     * if no error
     */
    public static String verifyCertificate(X509Certificate cert, Collection<CRL> crls, Calendar calendar) {
        if (calendar == null)
            calendar = new GregorianCalendar();
        if (cert.hasUnsupportedCriticalExtension()) {
        	for (String oid : cert.getCriticalExtensionOIDs()) {
    			// KEY USAGE and DIGITAL SIGNING is ALLOWED
        		if ("2.5.29.15".equals(oid) && cert.getKeyUsage()[0]) {
        			continue;
        		}
        		try {
        			// EXTENDED KEY USAGE and TIMESTAMPING is ALLOWED
					if ("2.5.29.37".equals(oid) && cert.getExtendedKeyUsage().contains("1.3.6.1.5.5.7.3.8")) {
						continue;
					}
				} catch (CertificateParsingException e) {
					// DO NOTHING;
				}
                return "Has unsupported critical extension";
        	}
        }
        try {
            cert.checkValidity(calendar.getTime());
        }
        catch (Exception e) {
            return e.getMessage();
        }
        if (crls != null) {
            for (CRL crl : crls) {
                if (crl.isRevoked(cert))
                    return "Certificate revoked";
            }
        }
        return null;
    }

	/**
	 * Verifies a certificate chain against a KeyStore.
	 * @param certs the certificate chain
	 * @param keystore the <CODE>KeyStore</CODE>
	 * @param crls the certificate revocation list or <CODE>null</CODE>
	 * @param calendar the date or <CODE>null</CODE> for the current date
	 * @return <CODE>null</CODE> if the certificate chain could be validated or a
	 * <CODE>Object[]{cert,error}</CODE> where <CODE>cert</CODE> is the
	 * failed certificate and <CODE>error</CODE> is the error message
	 */
	public static List<VerificationException> verifyCertificates(Certificate certs[], KeyStore keystore, Collection<CRL> crls, Calendar calendar) {
	    List<VerificationException> result = new ArrayList<VerificationException>();
		if (calendar == null)
	        calendar = new GregorianCalendar();
	    for (int k = 0; k < certs.length; ++k) {
	        X509Certificate cert = (X509Certificate)certs[k];
	        String err = verifyCertificate(cert, crls, calendar);
	        if (err != null)
	            result.add(new VerificationException(cert, err));
	        try {
	            for (Enumeration<String> aliases = keystore.aliases(); aliases.hasMoreElements();) {
	                try {
	                    String alias = aliases.nextElement();
	                    if (!keystore.isCertificateEntry(alias))
	                        continue;
	                    X509Certificate certStoreX509 = (X509Certificate)keystore.getCertificate(alias);
	                    if (verifyCertificate(certStoreX509, crls, calendar) != null)
	                        continue;
	                    try {
	                        cert.verify(certStoreX509.getPublicKey());
	                        return result;
	                    }
	                    catch (Exception e) {
	                        continue;
	                    }
	                }
	                catch (Exception ex) {
	                }
	            }
	        }
	        catch (Exception e) {
	        }
	        int j;
	        for (j = 0; j < certs.length; ++j) {
	            if (j == k)
	                continue;
	            X509Certificate certNext = (X509Certificate)certs[j];
	            try {
	                cert.verify(certNext.getPublicKey());
	                break;
	            }
	            catch (Exception e) {
	            }
	        }
	        if (j == certs.length) {
	        	result.add(new VerificationException(cert, "Cannot be verified against the KeyStore or the certificate chain"));
	        }
	    }
	    if (result.size() == 0)
	    	result.add(new VerificationException(null, "Invalid state. Possible circular certificate chain"));
	    return result;
	}

	/**
	 * Verifies a certificate chain against a KeyStore.
	 * @param certs the certificate chain
	 * @param keystore the <CODE>KeyStore</CODE>
	 * @param calendar the date or <CODE>null</CODE> for the current date
	 * @return <CODE>null</CODE> if the certificate chain could be validated or a
	 * <CODE>Object[]{cert,error}</CODE> where <CODE>cert</CODE> is the
	 * failed certificate and <CODE>error</CODE> is the error message
	 */
	public static List<VerificationException> verifyCertificates(Certificate certs[], KeyStore keystore, Calendar calendar) {
		return verifyCertificates(certs, keystore, null, calendar);
	}
	
	/**
	 * Verifies an OCSP response against a KeyStore.
	 * @param ocsp the OCSP response
	 * @param keystore the <CODE>KeyStore</CODE>
	 * @param provider the provider or <CODE>null</CODE> to use the BouncyCastle provider
	 * @return <CODE>true</CODE> is a certificate was found
	 */
	public static boolean verifyOcspCertificates(BasicOCSPResp ocsp, KeyStore keystore, String provider) {
	    if (provider == null)
	        provider = "BC";
	    try {
	        for (Enumeration<String> aliases = keystore.aliases(); aliases.hasMoreElements();) {
	            try {
	                String alias = aliases.nextElement();
	                if (!keystore.isCertificateEntry(alias))
	                    continue;
	                X509Certificate certStoreX509 = (X509Certificate)keystore.getCertificate(alias);
	                if (ocsp.isSignatureValid(new JcaContentVerifierProviderBuilder().setProvider(provider).build(certStoreX509.getPublicKey())))
	                    return true;
	            }
	            catch (Exception ex) {
	            }
	        }
	    }
	    catch (Exception e) {
	    }
	    return false;
	}

	/**
	 * Verifies a time stamp against a KeyStore.
	 * @param ts the time stamp
	 * @param keystore the <CODE>KeyStore</CODE>
	 * @param provider the provider or <CODE>null</CODE> to use the BouncyCastle provider
	 * @return <CODE>true</CODE> is a certificate was found
	 */
	public static boolean verifyTimestampCertificates(TimeStampToken ts, KeyStore keystore, String provider) {
	    if (provider == null)
	        provider = "BC";
	    try {
	        for (Enumeration<String> aliases = keystore.aliases(); aliases.hasMoreElements();) {
	            try {
	                String alias = aliases.nextElement();
	                if (!keystore.isCertificateEntry(alias))
	                    continue;
	                X509Certificate certStoreX509 = (X509Certificate)keystore.getCertificate(alias);
	                ts.isSignatureValid(new JcaSimpleSignerInfoVerifierBuilder().setProvider(provider).build(certStoreX509));
	                return true;
	            }
	            catch (Exception ex) {
	            }
	        }
	    }
	    catch (Exception e) {
	    }
	    return false;
	}

}
