/***** BEGIN LICENSE BLOCK *****
 * Version: EPL 1.0/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Eclipse Public
 * License Version 1.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.eclipse.org/legal/epl-v10.html
 *
 * Software distributed under the License is distributed on an "AS
 * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
 * implied. See the License for the specific language governing
 * rights and limitations under the License.
 *
 * Copyright (C) 2006 Ola Bini <ola@ologix.com>
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either of the GNU General Public License Version 2 or later (the "GPL"),
 * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the EPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the EPL, the GPL or the LGPL.
 ***** END LICENSE BLOCK *****/
package org.jruby.ext.openssl.x509store;

import java.security.GeneralSecurityException;
import java.security.PublicKey;
import java.security.cert.X509CRL;
import java.security.cert.X509Certificate;
import java.security.cert.X509Extension;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1Integer;
import org.bouncycastle.asn1.ASN1Sequence;
import org.jruby.ext.openssl.SecurityHelper;

/**
 * c: X509_STORE_CTX
 *
 * @author <a href="mailto:ola.bini@ki.se">Ola Bini</a>
 */
public class StoreContext {

    private static final Integer ZERO = 0;

    private final Store store;

    private int currentMethod;

    X509AuxCertificate certificate;
    List<X509AuxCertificate> untrusted;
    List<X509CRL> crls;

    public VerifyParameter verifyParameter;

    public List<X509AuxCertificate> otherContext;

    public StoreContext(final Store store) {
        this.store = store;
    }

    public static interface CheckPolicyFunction extends Function1<StoreContext> {
        public static final CheckPolicyFunction EMPTY = new CheckPolicyFunction(){
            public int call(StoreContext context) {
                return -1;
            }
        };
    }

    Store.VerifyFunction verify;
    Store.VerifyCallbackFunction verifyCallback;
    Store.GetIssuerFunction getIssuer;
    Store.CheckIssuedFunction checkIssued;
    Store.CheckRevocationFunction checkRevocation;
    Store.GetCRLFunction getCRL;
    Store.CheckCRLFunction checkCRL;
    Store.CertificateCRLFunction certificateCRL;
    CheckPolicyFunction checkPolicy;
    Store.CleanupFunction cleanup;

    public boolean isValid;
    public int lastUntrusted;

    public List<X509AuxCertificate> chain; //List<X509AuxCertificate>
    public PolicyTree tree;

    public int explicitPolicy;

    public int error;
    public int errorDepth;

    X509AuxCertificate currentCertificate;
    X509AuxCertificate currentIssuer;
    X509CRL currentCRL;

    List<Object> extraData;

    public Store getStore() {
        return store;
    }

    /**
     * c: X509_STORE_CTX_set_depth
     */
    public void setDepth(int depth) {
        verifyParameter.setDepth(depth);
    }

    /**
     * c: X509_STORE_CTX_set_app_data
     */
    public void setApplicationData(Object data) {
        setExtraData(0, data);
    }

    /**
     * c: X509_STORE_CTX_get_app_data
     */
    public Object getApplicationData() {
        return getExtraData(0);
    }

    /**
     * c: X509_STORE_CTX_get1_issuer
     */
    int getFirstIssuer(final X509AuxCertificate[] issuers, final X509AuxCertificate x) throws Exception {
        final Name xn = new Name( x.getIssuerX500Principal() );
        final X509Object[] s_obj = new X509Object[1];
        int ok = store == null ? 0 : getBySubject(X509Utils.X509_LU_X509, xn, s_obj);
        if ( ok != X509Utils.X509_LU_X509 ) {
            if ( ok == X509Utils.X509_LU_RETRY ) {
                X509Error.addError(X509Utils.X509_R_SHOULD_RETRY);
                return -1;
            }
            else if ( ok != X509Utils.X509_LU_FAIL ) {
                return -1;
            }
            return 0;
        }

        X509Object obj = s_obj[0];
        if ( checkIssued.call(this, x, ((Certificate) obj).x509) != 0 ) {
            issuers[0] = ((Certificate) obj).x509;
            return 1;
        }

        List<X509Object> objects = store.getObjects();
        int idx = X509Object.indexBySubject(objects, X509Utils.X509_LU_X509, xn);
        if ( idx == -1 ) return 0;

        /* Look through all matching certificates for a suitable issuer */
        for ( int i = idx; i < objects.size(); i++ ) {
            final X509Object pobj = objects.get(i);
            if ( pobj.type() != X509Utils.X509_LU_X509 ) {
                return 0;
            }
            final X509AuxCertificate x509 = ((Certificate) pobj).x509;
            if ( ! xn.equalTo( x509.getSubjectX500Principal() ) ) {
                return 0;
            }
            if ( checkIssued.call(this, x, x509) != 0 ) {
                issuers[0] = x509;
                return 1;
            }
        }
        return 0;
    }

    public static List<X509AuxCertificate> ensureAux(final Collection<X509Certificate> input) {
        if ( input == null ) return null;

        List<X509AuxCertificate> out = new ArrayList<X509AuxCertificate>(input.size());
        for ( X509Certificate cert : input ) out.add( ensureAux(cert) );
        return out;
    }

    public static List<X509AuxCertificate> ensureAux(final X509Certificate[] input) {
        if ( input == null ) return null;

        List<X509AuxCertificate> out = new ArrayList<X509AuxCertificate>(input.length);
        for ( X509Certificate cert : input ) out.add( ensureAux(cert) );
        return out;
    }

    public static X509AuxCertificate ensureAux(final X509Certificate input) {
        if ( input == null ) return null;

        if ( input instanceof X509AuxCertificate ) {
            return (X509AuxCertificate) input;
        }
        return new X509AuxCertificate(input);
    }

    /**
     * c: X509_STORE_CTX_init
     */
    public int init(X509AuxCertificate cert, List<X509AuxCertificate> chain) {
        int ret = 1;
        this.currentMethod = 0;
        this.certificate = cert;
        this.untrusted = chain;
        this.crls = null;
        this.lastUntrusted = 0;
        this.otherContext = null;
        this.isValid = false;
        this.chain = null;
        this.error = 0;
        this.explicitPolicy = 0;
        this.errorDepth = 0;
        this.currentCertificate = null;
        this.currentIssuer = null;
        this.tree = null;

        this.verifyParameter = new VerifyParameter();

        if ( store != null ) {
            ret = verifyParameter.inherit(store.verifyParameter);
        } else {
            verifyParameter.flags |= X509Utils.X509_VP_FLAG_DEFAULT | X509Utils.X509_VP_FLAG_ONCE;
        }

        if ( store != null ) {
            verifyCallback = store.getVerifyCallback();
            cleanup = store.cleanup;
        } else {
            cleanup = Store.CleanupFunction.EMPTY;
        }

        if ( ret != 0 ) {
            ret = verifyParameter.inherit(VerifyParameter.lookup("default"));
        }

        if ( ret == 0 ) {
            X509Error.addError(X509Utils.ERR_R_MALLOC_FAILURE);
            return 0;
        }

        this.checkIssued = defaultCheckIssued;
        this.getIssuer = getFirstIssuer;
        this.verifyCallback = nullCallback;
        this.verify = internalVerify;
        this.checkRevocation = defaultCheckRevocation;
        this.getCRL = defaultGetCRL;
        this.checkCRL = defaultCheckCRL;
        this.certificateCRL = defaultCertificateCRL;

        if ( store != null ) {
            if ( store.checkIssued != null && store.checkIssued != Store.CheckIssuedFunction.EMPTY ) {
                this.checkIssued = store.checkIssued;
            }
            if ( store.getIssuer != null && store.getIssuer != Store.GetIssuerFunction.EMPTY ) {
                this.getIssuer = store.getIssuer;
            }
            if ( store.verifyCallback != null && store.verifyCallback != Store.VerifyCallbackFunction.EMPTY ) {
                this.verifyCallback = store.verifyCallback;
            }
            if ( store.verify != null && store.verify != Store.VerifyFunction.EMPTY) {
                this.verify = store.verify;
            }
            if ( store.checkRevocation != null && store.checkRevocation != Store.CheckRevocationFunction.EMPTY) {
                this.checkRevocation = store.checkRevocation;
            }
            if ( store.getCRL != null && store.getCRL != Store.GetCRLFunction.EMPTY) {
                this.getCRL = store.getCRL;
            }
            if( store.checkCRL != null && store.checkCRL != Store.CheckCRLFunction.EMPTY) {
                this.checkCRL = store.checkCRL;
            }
            if ( store.certificateCRL != null && store.certificateCRL != Store.CertificateCRLFunction.EMPTY) {
                this.certificateCRL = store.certificateCRL;
            }
        }

        this.checkPolicy = defaultCheckPolicy;

        // getExtraData();
        return 1;
    }

    /**
     * c: X509_STORE_CTX_trusted_stack
     */
    public void trustedStack(List<X509AuxCertificate> sk) {
        otherContext = sk;
        getIssuer = getIssuerStack;
    }

    /**
     * c: X509_STORE_CTX_cleanup
     */
    public void cleanup() throws Exception {
        if (cleanup != null && cleanup != Store.CleanupFunction.EMPTY) {
            cleanup.call(this);
        }
        verifyParameter = null;
        tree = null;
        chain = null;
        extraData = null;
    }

    /**
     * c: find_issuer
     */
    public X509AuxCertificate findIssuer(final List<X509AuxCertificate> certs, final X509AuxCertificate cert) throws Exception {
        for ( X509AuxCertificate issuer : certs ) {
            if ( checkIssued.call(this, cert, issuer) != 0 ) {
                return issuer;
            }
        }
        return null;
    }

    public List<Object> getExtraData() {
        if ( this.extraData != null ) return this.extraData;
        ArrayList<Object> extraData = new ArrayList<Object>(8);
        extraData.add(null); extraData.add(null); extraData.add(null);
        extraData.add(null); extraData.add(null); extraData.add(null);
        return this.extraData = extraData;
    }

    /**
     * c: X509_STORE_CTX_set_ex_data
     */
    public int setExtraData(int idx, Object data) {
        getExtraData().set(idx, data);
        return 1;
    }

    /**
     * c: X509_STORE_CTX_get_ex_data
     */
    public Object getExtraData(int idx) {
        return getExtraData().get(idx);
    }

    /**
     * c: X509_STORE_CTX_get_error
     */
    public int getError() {
        return error;
    }

    /**
     * c: X509_STORE_CTX_set_error
     */
    public void setError(int s) {
        this.error = s;
    }

    /**
     * c: X509_STORE_CTX_get_error_depth
     */
    public int getErrorDepth() {
        return errorDepth;
    }

    /**
     * c: X509_STORE_CTX_get_current_cert
     */
    public X509AuxCertificate getCurrentCertificate() {
        return currentCertificate;
    }

    public X509CRL getCurrentCRL() {
        return currentCRL;
    }

    /**
     * c: X509_STORE_CTX_get_chain
     */
    public List<X509AuxCertificate> getChain() {
        return chain;
    }

    /**
     * c: X509_STORE_CTX_get1_chain
     */
    public List<X509AuxCertificate> getFirstChain() {
        if ( chain == null ) return null;
        return new ArrayList<X509AuxCertificate>(chain);
    }

    /**
     * c: X509_STORE_CTX_set_cert
     */
    public void setCertificate(X509AuxCertificate x) {
        this.certificate = x;
    }

    public void setCertificate(X509Certificate x) {
        this.certificate = ensureAux(x);
    }

    /**
     * c: X509_STORE_CTX_set_chain
     */
    public void setChain(List<X509Certificate> chain) {
        this.untrusted = ensureAux(chain);
    }

    public void setChain(X509Certificate[] sk) {
        this.untrusted = ensureAux(sk);
    }

    /**
     * c: X509_STORE_CTX_set0_crls
     */
    public void setCRLs(List<X509CRL> sk) {
        this.crls = sk;
    }

    /**
     * c: X509_STORE_CTX_set_purpose
     */
    public int setPurpose(int purpose) {
        return purposeInherit(0, purpose, 0);
    }

    /**
     * c: X509_STORE_CTX_set_trust
     */
    public int setTrust(int trust) {
        return purposeInherit(0, 0, trust);
    }

    /*
    private void resetSettingsToWithoutStore() {
        store = null;
        this.verifyParameter = new VerifyParameter();
        this.verifyParameter.flags |= X509Utils.X509_VP_FLAG_DEFAULT | X509Utils.X509_VP_FLAG_ONCE;
        this.verifyParameter.inherit(VerifyParameter.lookup("default"));
        this.cleanup = Store.CleanupFunction.EMPTY;
        this.checkIssued = defaultCheckIssued;
        this.getIssuer = getFirstIssuer;
        this.verifyCallback = nullCallback;
        this.verify = internalVerify;
        this.checkRevocation = defaultCheckRevocation;
        this.getCRL = defaultGetCRL;
        this.checkCRL = defaultCheckCRL;
        this.certificateCRL = defaultCertificateCRL;
    } */

    /**
     * c: SSL_CTX_load_verify_locations
     */
    /*
    public int loadVerifyLocations(Ruby runtime, String CAfile, String CApath) {
        boolean reset = false;
        try {
            if ( store == null ) {
                reset = true;
                store = new Store();
                this.verifyParameter.inherit(store.verifyParameter);
                verifyParameter.inherit(VerifyParameter.lookup("default"));
                this.cleanup = store.cleanup;
                if ( store.checkIssued != null && store.checkIssued != Store.CheckIssuedFunction.EMPTY ) {
                    this.checkIssued = store.checkIssued;
                }
                if ( store.getIssuer != null && store.getIssuer != Store.GetIssuerFunction.EMPTY ) {
                    this.getIssuer = store.getIssuer;
                }
                if ( store.verify != null && store.verify != Store.VerifyFunction.EMPTY ) {
                    this.verify = store.verify;
                }
                if ( store.verifyCallback != null && store.verifyCallback != Store.VerifyCallbackFunction.EMPTY ) {
                    this.verifyCallback = store.verifyCallback;
                }
                if ( store.checkRevocation != null && store.checkRevocation != Store.CheckRevocationFunction.EMPTY ) {
                    this.checkRevocation = store.checkRevocation;
                }
                if ( store.getCRL != null && store.getCRL != Store.GetCRLFunction.EMPTY ) {
                    this.getCRL = store.getCRL;
                }
                if ( store.checkCRL != null && store.checkCRL != Store.CheckCRLFunction.EMPTY ) {
                    this.checkCRL = store.checkCRL;
                }
                if ( store.certificateCRL != null && store.certificateCRL != Store.CertificateCRLFunction.EMPTY ) {
                    this.certificateCRL = store.certificateCRL;
                }
            }

            final int ret = store.loadLocations(runtime, CAfile, CApath);
            if ( ret == 0 && reset ) resetSettingsToWithoutStore();

            return ret;
        }
        catch (Exception e) {

            if ( reset ) resetSettingsToWithoutStore();
            return 0;
        }
    } */

    /**
     * c: X509_STORE_CTX_purpose_inherit
     */
    public int purposeInherit(int defaultPurpose,int purpose, int trust) {
        int idx;
        if(purpose == 0) {
            purpose = defaultPurpose;
        }
        if(purpose != 0) {
            idx = Purpose.getByID(purpose);
            if(idx == -1) {
                X509Error.addError(X509Utils.X509_R_UNKNOWN_PURPOSE_ID);
                return 0;
            }
            Purpose ptmp = Purpose.getFirst(idx);
            if(ptmp.trust == X509Utils.X509_TRUST_DEFAULT) {
                idx = Purpose.getByID(defaultPurpose);
                if(idx == -1) {
                    X509Error.addError(X509Utils.X509_R_UNKNOWN_PURPOSE_ID);
                    return 0;
                }
                ptmp = Purpose.getFirst(idx);
            }
            if(trust == 0) {
                trust = ptmp.trust;
            }
        }
        if(trust != 0) {
            idx = Trust.getByID(trust);
            if(idx == -1) {
                X509Error.addError(X509Utils.X509_R_UNKNOWN_TRUST_ID);
                return 0;
            }
        }

        if(purpose != 0 && verifyParameter.purpose == 0) {
            verifyParameter.purpose = purpose;
        }
        if(trust != 0 && verifyParameter.trust == 0) {
            verifyParameter.trust = trust;
        }
        return 1;
    }

    /**
     * c: X509_STORE_CTX_set_flags
     */
    public void setFlags(long flags) {
        verifyParameter.setFlags(flags);
    }

    /**
     * c: X509_STORE_CTX_set_time
     */
    public void setTime(long flags,Date t) {
        verifyParameter.setTime(t);
    }

    /**
     * c: X509_STORE_CTX_set_verify_cb
     */
    public void setVerifyCallback(Store.VerifyCallbackFunction verifyCallback) {
        this.verifyCallback = verifyCallback;
    }

    /**
     * c: X509_STORE_CTX_get0_policy_tree
     */
    PolicyTree getPolicyTree() {
        return tree;
    }

    /**
     * c: X509_STORE_CTX_get_explicit_policy
     */
    public int getExplicitPolicy() {
        return explicitPolicy;
    }

    /**
     * c: X509_STORE_CTX_get0_param
     */
    public VerifyParameter getParam() {
        return verifyParameter;
    }

    /**
     * c: X509_STORE_CTX_set0_param
     */
    public void setParam(VerifyParameter param) {
        this.verifyParameter = param;
    }

    /**
     * c: X509_STORE_CTX_set_default
     */
    public int setDefault(String name) {
        VerifyParameter p = VerifyParameter.lookup(name);
        if ( p == null ) return 0;
        return verifyParameter.inherit(p);
    }

    /**
     * c: X509_STORE_get_by_subject (it gets X509_STORE_CTX as the first parameter)
     */
    public int getBySubject(int type,Name name,X509Object[] ret) throws Exception {
        Store c = store;

        X509Object tmp = X509Object.retrieveBySubject(c.getObjects(),type,name);
        if ( tmp == null ) {
            List<Lookup> certificateMethods = c.getCertificateMethods();
            for(int i=currentMethod; i<certificateMethods.size(); i++) {
                Lookup lu = certificateMethods.get(i);
                X509Object[] stmp = new X509Object[1];
                int j = lu.bySubject(type,name,stmp);
                if ( j < 0 ) {
                    currentMethod = i;
                    return j;
                }
                else if( j > 0 ) {
                    tmp = stmp[0];
                    break;
                }
            }
            currentMethod = 0;

            if ( tmp == null ) return 0;
        }
        ret[0] = tmp;
        return 1;
    }

    /**
     * c: X509_verify_cert
     */
    public int verifyCertificate() throws Exception {
        X509AuxCertificate x, xtmp = null, chain_ss = null;
        //X509_NAME xn;
        int bad_chain = 0, depth, i, num;

        if ( certificate == null ) {
            X509Error.addError(X509Utils.X509_R_NO_CERT_SET_FOR_US_TO_VERIFY);
            return -1;
        }

        // first we make sure the chain we are going to build is
        // present and that the first entry is in place

        if ( chain == null ) {
            chain = new ArrayList<X509AuxCertificate>();
            chain.add(certificate);
            lastUntrusted = 1;
        }

        // We use a temporary STACK so we can chop and hack at it

        List<X509AuxCertificate> sktmp = null;
        if ( untrusted != null ) {
            sktmp = new ArrayList<X509AuxCertificate>(untrusted);
        }
        num = chain.size();
        x = chain.get(num - 1);
        depth = verifyParameter.depth;
        for(;;) {
            if ( depth < num ) break;

            if ( checkIssued.call(this, x, x) != 0 ) break;

            if ( sktmp != null ) {
                xtmp = findIssuer(sktmp, x);
                if ( xtmp != null ) {
                    chain.add(xtmp);
                    sktmp.remove(xtmp);
                    lastUntrusted++;
                    x = xtmp;
                    num++;
                    continue;
                }
            }
            break;
        }

        // at this point, chain should contain a list of untrusted
        // certificates.  We now need to add at least one trusted one,
        // if possible, otherwise we complain.

        // Examine last certificate in chain and see if it is self signed.

        i = chain.size();
        x = chain.get(i - 1);

        if ( checkIssued.call(this, x, x) != 0 ) {
            // we have a self signed certificate
            if ( chain.size() == 1 ) {
                // We have a single self signed certificate: see if
                // we can find it in the store. We must have an exact
                // match to avoid possible impersonation.
                X509AuxCertificate[] p_xtmp = new X509AuxCertificate[]{ xtmp };
                int ok = getIssuer.call(this, p_xtmp, x);
                xtmp = p_xtmp[0];
                if ( ok <= 0 || ! x.equals(xtmp) ) {
                    error = X509Utils.V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT;
                    currentCertificate = x;
                    errorDepth = i-1;
                    bad_chain = 1;
                    ok = verifyCallback.call(this, ZERO);
                    if ( ok == 0 ) return ok;
                } else {
                    // We have a match: replace certificate with store version
                    // so we get any trust settings.
                    x = xtmp;
                    chain.set(i-1,x);
                    lastUntrusted = 0;
                }
            } else {
                // extract and save self signed certificate for later use
                chain_ss = chain.remove(chain.size()-1);
                lastUntrusted--;
                num--;
                x = chain.get(num-1);
            }
        }
        // We now lookup certs from the certificate store
        for(;;) {
            // If we have enough, we break
            if ( depth < num ) break;
            //xn = new X509_NAME(x.getIssuerX500Principal());
            // If we are self signed, we break
            if ( checkIssued.call(this, x, x) != 0 ) break;

            X509AuxCertificate[] p_xtmp = new X509AuxCertificate[]{ xtmp };
            int ok = getIssuer.call(this, p_xtmp, x);
            xtmp = p_xtmp[0];

            if ( ok < 0 ) return ok;
            if ( ok == 0 ) break;

            x = xtmp;
            chain.add(x);
            num++;
        }

        /* we now have our chain, lets check it... */

        //xn = new X509_NAME(x.getIssuerX500Principal());
        /* Is last certificate looked up self signed? */
        if ( checkIssued.call(this, x, x) == 0 ) {
            if ( chain_ss == null || checkIssued.call(this, x, chain_ss) == 0 ) {
                if(lastUntrusted >= num) {
                    error = X509Utils.V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY;
                } else {
                    error = X509Utils.V_ERR_UNABLE_TO_GET_ISSUER_CERT;
                }
                currentCertificate = x;
            } else {
                chain.add(chain_ss);
                num++;
                lastUntrusted = num;
                currentCertificate = chain_ss;
                error = X509Utils.V_ERR_SELF_SIGNED_CERT_IN_CHAIN;
            }
            errorDepth = num - 1;
            bad_chain = 1;
            int ok = verifyCallback.call(this, ZERO);
            if ( ok == 0 ) return ok;
        }

        // We have the chain complete: now we need to check its purpose
        int ok = checkChainExtensions();
        if ( ok == 0 ) return ok;

        /* TODO: Check name constraints (from 1.0.0) */

        // The chain extensions are OK: check trust
        if ( verifyParameter.trust > 0 ) ok = checkTrust();
        if ( ok == 0 ) return ok;

        // Check revocation status: we do this after copying parameters
        // because they may be needed for CRL signature verification.
        ok = checkRevocation.call(this);
        if ( ok == 0 ) return ok;

        /* At this point, we have a chain and need to verify it */
        if ( verify != null && verify != Store.VerifyFunction.EMPTY ) {
            ok = verify.call(this);
        } else {
            ok = internalVerify.call(this);
        }
        if ( ok == 0 ) return ok;

        /* TODO: RFC 3779 path validation, now that CRL check has been done (from 1.0.0) */

        /* If we get this far evaluate policies */
        if ( bad_chain == 0 && (verifyParameter.flags & X509Utils.V_FLAG_POLICY_CHECK) != 0 ) {
            ok = checkPolicy.call(this);
        }
        return ok;
    }


    private final static Set<String> CRITICAL_EXTENSIONS = new HashSet<String>(8);
    static {
        CRITICAL_EXTENSIONS.add("2.16.840.1.113730.1.1"); // netscape cert type, NID 71
        CRITICAL_EXTENSIONS.add("2.5.29.15"); // key usage, NID 83
        CRITICAL_EXTENSIONS.add("2.5.29.17"); // subject alt name, NID 85
        CRITICAL_EXTENSIONS.add("2.5.29.19"); // basic constraints, NID 87
        CRITICAL_EXTENSIONS.add("2.5.29.37"); // ext key usage, NID 126
        CRITICAL_EXTENSIONS.add("1.3.6.1.5.5.7.1.14"); // proxy cert info, NID 661
    }

    private static boolean supportsCriticalExtension(final String oid) {
        return CRITICAL_EXTENSIONS.contains(oid);
    }

    private static boolean unhandledCritical(final X509Extension ext) {
        final Set<String> criticalOIDs = ext.getCriticalExtensionOIDs();
        if ( criticalOIDs == null || criticalOIDs.size() == 0 ) {
            return false;
        }
        for ( final String oid : criticalOIDs ) {
            if ( ! supportsCriticalExtension(oid) ) return true;
        }
        return false;
    }

    /**
     * c: check_chain_extensions
     */
    public int checkChainExtensions() throws Exception {
        int ok, must_be_ca;
        X509AuxCertificate x;
        int proxy_path_length = 0;
        int allow_proxy_certs = (verifyParameter.flags & X509Utils.V_FLAG_ALLOW_PROXY_CERTS) != 0 ? 1 : 0;
        must_be_ca = -1;

        try {
            final String allowProxyCerts = System.getenv("OPENSSL_ALLOW_PROXY_CERTS");
            if ( allowProxyCerts != null && ! "false".equalsIgnoreCase(allowProxyCerts) ) {
                allow_proxy_certs = 1;
            }
        }
        catch (SecurityException e) { /* ignore if we can't use System.getenv */ }

        for ( int i = 0; i < lastUntrusted; i++ ) {
            int ret;
            x = chain.get(i);
            if ( (verifyParameter.flags & X509Utils.V_FLAG_IGNORE_CRITICAL) == 0 && unhandledCritical(x) ) {
                error = X509Utils.V_ERR_UNHANDLED_CRITICAL_EXTENSION;
                errorDepth = i;
                currentCertificate = x;
                ok = verifyCallback.call(this, ZERO);
                if ( ok == 0 ) return ok;
            }
            if ( allow_proxy_certs == 0 && x.getExtensionValue("1.3.6.1.5.5.7.1.14") != null ) {
                error = X509Utils.V_ERR_PROXY_CERTIFICATES_NOT_ALLOWED;
                errorDepth = i;
                currentCertificate = x;
                ok = verifyCallback.call(this, ZERO);
                if ( ok == 0 ) return ok;
            }

            ret = Purpose.checkCA(x);
            switch(must_be_ca) {
            case -1:
                if((verifyParameter.flags & X509Utils.V_FLAG_X509_STRICT) != 0 && ret != 1 && ret != 0) {
                    ret = 0;
                    error = X509Utils.V_ERR_INVALID_CA;
                } else {
                    ret = 1;
                }
                break;
            case 0:
                if(ret != 0) {
                    ret = 0;
                    error = X509Utils.V_ERR_INVALID_NON_CA;
                } else {
                    ret = 1;
                }
                break;
            default:
                if(ret == 0 || ((verifyParameter.flags & X509Utils.V_FLAG_X509_STRICT) != 0 && ret != 1)) {
                    ret = 0;
                    error = X509Utils.V_ERR_INVALID_CA;
                } else {
                    ret = 1;
                }
                break;
            }
            if(ret == 0) {
                errorDepth = i;
                currentCertificate = x;
                ok = verifyCallback.call(this, ZERO);
                if ( ok == 0 ) return ok;
            }
            if(verifyParameter.purpose > 0) {
                ret = Purpose.checkPurpose(x,verifyParameter.purpose, must_be_ca > 0 ? 1 : 0);
                if(ret == 0 || ((verifyParameter.flags & X509Utils.V_FLAG_X509_STRICT) != 0 && ret != 1)) {
                    error = X509Utils.V_ERR_INVALID_PURPOSE;
                    errorDepth = i;
                    currentCertificate = x;
                    ok = verifyCallback.call(this, ZERO);
                    if(ok == 0) {
                        return ok;
                    }
                }
            }

            if(i > 1 && x.getBasicConstraints() != -1 && x.getBasicConstraints() != Integer.MAX_VALUE && (i > (x.getBasicConstraints() + proxy_path_length + 1))) {
                error = X509Utils.V_ERR_PATH_LENGTH_EXCEEDED;
                errorDepth = i;
                currentCertificate = x;
                ok = verifyCallback.call(this, ZERO);
                if ( ok == 0 ) return ok;
            }

            if(x.getExtensionValue("1.3.6.1.5.5.7.1.14") != null) {
                ASN1Sequence pci = (ASN1Sequence)new ASN1InputStream(x.getExtensionValue("1.3.6.1.5.5.7.1.14")).readObject();
                if(pci.size() > 0 && pci.getObjectAt(0) instanceof ASN1Integer) {
                    int pcpathlen = ((ASN1Integer)pci.getObjectAt(0)).getValue().intValue();
                    if(i > pcpathlen) {
                        error = X509Utils.V_ERR_PROXY_PATH_LENGTH_EXCEEDED;
                        errorDepth = i;
                        currentCertificate = x;
                        ok = verifyCallback.call(this, ZERO);
                        if ( ok == 0 ) return ok;
                    }
                }
                proxy_path_length++;
                must_be_ca = 0;
            } else {
                must_be_ca = 1;
            }
        }
        return 1;
    }

    /**
     * c: X509_check_trust
     */
    public int checkTrust() throws Exception {
        int i,ok;
        X509AuxCertificate x;
        i = chain.size()-1;
        x = chain.get(i);
        ok = Trust.checkTrust(x, verifyParameter.trust, 0);
        if ( ok == X509Utils.X509_TRUST_TRUSTED ) {
            return 1;
        }
        errorDepth = 1;
        currentCertificate = x;
        if ( ok == X509Utils.X509_TRUST_REJECTED ) {
            error = X509Utils.V_ERR_CERT_REJECTED;
        } else {
            error = X509Utils.V_ERR_CERT_UNTRUSTED;
        }
        return verifyCallback.call(this, ZERO);
    }

    /**
     * c: check_cert_time
     */
    public int checkCertificateTime(X509AuxCertificate x) throws Exception {
        final Date pTime;
        if ( (verifyParameter.flags & X509Utils.V_FLAG_USE_CHECK_TIME) != 0 ) {
            pTime = this.verifyParameter.checkTime;
        } else {
            pTime = Calendar.getInstance().getTime();
        }

        if ( ! x.getNotBefore().before(pTime) ) {
            error = X509Utils.V_ERR_CERT_NOT_YET_VALID;
            currentCertificate = x;
            if ( verifyCallback.call(this, ZERO) == 0 ) {
                return 0;
            }
        }
        if ( ! x.getNotAfter().after(pTime) ) {
            error = X509Utils.V_ERR_CERT_HAS_EXPIRED;
            currentCertificate = x;
            if ( verifyCallback.call(this, ZERO) == 0 ) {
                return 0;
            }
        }
        return 1;
    }

    /**
     * c: check_cert
     */
    public int checkCertificate() throws Exception {
        final X509CRL[] crl = new X509CRL[1];
        X509AuxCertificate x;
        int ok, cnum;
        cnum = errorDepth;
        x = chain.get(cnum);
        currentCertificate = x;
        ok = getCRL.call(this, crl, x);
        if ( ok == 0 ) {
            error = X509Utils.V_ERR_UNABLE_TO_GET_CRL;
            ok = verifyCallback.call(this, ZERO);
            currentCRL = null;
            return ok;
        }
        currentCRL = crl[0];
        ok = checkCRL.call(this, crl[0]);
        if ( ok == 0 ) {
            currentCRL = null;
            return ok;
        }
        ok = certificateCRL.call(this, crl[0], x);
        currentCRL = null;
        return ok;
    }

    /**
     * c: check_crl_time
     */
    public int checkCRLTime(X509CRL crl, int notify) throws Exception {
        currentCRL = crl;
        final Date pTime;
        if ( (verifyParameter.flags & X509Utils.V_FLAG_USE_CHECK_TIME) != 0 ) {
            pTime = this.verifyParameter.checkTime;
        } else {
            pTime = Calendar.getInstance().getTime();
        }

        if ( ! crl.getThisUpdate().before(pTime) ) {
            error = X509Utils.V_ERR_CRL_NOT_YET_VALID;
            if ( notify == 0 || verifyCallback.call(this, ZERO) == 0 ) {
                return 0;
            }
        }
        if ( crl.getNextUpdate() != null && !crl.getNextUpdate().after(pTime) ) {
            error = X509Utils.V_ERR_CRL_HAS_EXPIRED;
            if ( notify == 0 || verifyCallback.call(this, ZERO) == 0 ) {
                return 0;
            }
        }

        currentCRL = null;
        return 1;
    }

    /**
     * c: get_crl_sk
     */
    public int getCRLStack(X509CRL[] pcrl, Name name, List<X509CRL> crls) throws Exception {
        X509CRL bestCrl = null;
        if ( crls != null ) {
            for ( final X509CRL crl : crls ) {
                if( ! name.equalTo( crl.getIssuerX500Principal() ) ) {
                    continue;
                }
                if ( checkCRLTime(crl, 0) != 0 ) {
                    pcrl[0] = crl;
                    return 1;
                }
                bestCrl = crl;
            }
        }
        if ( bestCrl != null ) {
            pcrl[0] = bestCrl;
        }
        return 0;
    }

    final static Store.GetIssuerFunction getFirstIssuer = new Store.GetIssuerFunction() {
        public int call(StoreContext context, X509AuxCertificate[] issuer, X509AuxCertificate cert) throws Exception {
            return context.getFirstIssuer(issuer, cert);
        }
    };

    /**
     * c: get_issuer_sk
     */
    final static Store.GetIssuerFunction getIssuerStack = new Store.GetIssuerFunction() {
        public int call(StoreContext context, X509AuxCertificate[] issuer, X509AuxCertificate x) throws Exception {
            issuer[0] = context.findIssuer(context.otherContext, x);
            if ( issuer[0] != null ) {
                return 1;
            } else {
                return 0;
            }
        }
    };

    /**
     * c: check_issued
     */
    final static Store.CheckIssuedFunction defaultCheckIssued = new Store.CheckIssuedFunction() {
        public int call(StoreContext context, X509AuxCertificate cert, X509AuxCertificate issuer) throws Exception {
            int ret = X509Utils.checkIfIssuedBy(issuer, cert);
            if ( ret == X509Utils.V_OK ) return 1;

            if ( (context.verifyParameter.flags & X509Utils.V_FLAG_CB_ISSUER_CHECK) == 0 ) {
                return 0;
            }
            context.error = ret;
            context.currentCertificate = cert;
            context.currentIssuer = issuer;

            return context.verifyCallback.call(context, ZERO);
        }
    };

    /**
     * c: null_callback
     */
    final static Store.VerifyCallbackFunction nullCallback = new Store.VerifyCallbackFunction() {
        public int call(StoreContext context, Integer outcome) {
            return outcome.intValue();
        }
    };

    /**
     * c: internal_verify
     */
    final static Store.VerifyFunction internalVerify = new Store.VerifyFunction() {
        public int call(final StoreContext context) throws Exception {
            Store.VerifyCallbackFunction verifyCallback = context.verifyCallback;

            int n = context.chain.size();
            context.errorDepth = n - 1;
            n--;
            X509AuxCertificate xi = context.chain.get(n);
            X509AuxCertificate xs = null;
            int ok;

            if ( context.checkIssued.call(context,xi,xi) != 0 ) {
                xs = xi;
            }
            else {
                if ( n <= 0 ) {
                    context.error = X509Utils.V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE;
                    context.currentCertificate = xi;
                    ok = verifyCallback.call(context, ZERO);
                    return ok;
                }
                else {
                    n--;
                    context.errorDepth = n;
                    xs = context.chain.get(n);
                }
            }

            while ( n >= 0 ) {
                context.errorDepth = n;
                if ( ! xs.isValid() ) {
                    try {
                        xs.verify(xi.getPublicKey());
                    }
                    catch(Exception e) {
                        /*
                        System.err.println("n: " + n);
                        System.err.println("verifying: " + xs);
                        System.err.println("verifying with issuer?: " + xi);
                        System.err.println("verifying with issuer.key?: " + xi.getPublicKey());
                        System.err.println("exception: " + e);
                        */
                        context.error = X509Utils.V_ERR_CERT_SIGNATURE_FAILURE;
                        context.currentCertificate = xs;
                        ok = verifyCallback.call(context, ZERO);
                        if ( ok == 0 ) return ok;
                    }
                }

                xs.setValid(true);
                ok = context.checkCertificateTime(xs);
                if ( ok == 0 ) return ok;

                context.currentIssuer = xi;
                context.currentCertificate = xs;
                ok = verifyCallback.call(context, Integer.valueOf(1));
                if ( ok == 0 ) return ok;

                n--;
                if ( n >= 0 ) {
                    xi = xs;
                    xs = context.chain.get(n);
                }
            }
            ok = 1;
            return ok;
        }
    };

    /**
     * c: check_revocation
     */
    final static Store.CheckRevocationFunction defaultCheckRevocation = new Store.CheckRevocationFunction() {
        public int call(final StoreContext context) throws Exception {
            if ( (context.verifyParameter.flags & X509Utils.V_FLAG_CRL_CHECK) == 0 ) {
                return 1;
            }
            final int last;
            if ( (context.verifyParameter.flags & X509Utils.V_FLAG_CRL_CHECK_ALL) != 0 ) {
                last = context.chain.size() - 1;
            }
            else {
                last = 0;
            }
            int ok;
            for ( int i=0; i<=last; i++ ) {
                context.errorDepth = i;
                ok = context.checkCertificate();
                if ( ok == 0 ) return 0;
            }
            return 1;
        }
    };

    /**
     * c: get_crl
     */
    final static Store.GetCRLFunction defaultGetCRL = new Store.GetCRLFunction() {
        public int call(final StoreContext context, final X509CRL[] crls, X509AuxCertificate x) throws Exception {
            final Name name = new Name( x.getIssuerX500Principal() );
            final X509CRL[] crl = new X509CRL[1];
            int ok = context.getCRLStack(crl, name, context.crls);
            if ( ok != 0 ) {
                crls[0] = crl[0];
                return 1;
            }
            final X509Object[] xobj = new X509Object[1];
            ok = context.getBySubject(X509Utils.X509_LU_CRL, name, xobj);
            if ( ok == 0 ) {
                if ( crl[0] != null ) {
                    crls[0] = crl[0];
                    return 1;
                }
                return 0;
            }
            crls[0] = (X509CRL) ( (CRL) xobj[0] ).crl;
            return 1;
        }
    };

    /**
     * c: check_crl
     */
    final static Store.CheckCRLFunction defaultCheckCRL = new Store.CheckCRLFunction() {
        public int call(final StoreContext context, final X509CRL crl) throws Exception {
            final int errorDepth = context.errorDepth;
            final int lastInChain = context.chain.size() - 1;

            int ok;
            final X509AuxCertificate issuer;
            if ( errorDepth < lastInChain ) {
                issuer = context.chain.get(errorDepth + 1);
            }
            else {
                issuer = context.chain.get(lastInChain);
                if ( context.checkIssued.call(context,issuer,issuer) == 0 ) {
                    context.error = X509Utils.V_ERR_UNABLE_TO_GET_CRL_ISSUER;
                    ok = context.verifyCallback.call(context, ZERO);
                    if ( ok == 0 ) return ok;
                }
            }

            if ( issuer != null ) {
                if ( issuer.getKeyUsage() != null && ! issuer.getKeyUsage()[6] ) {
                    context.error = X509Utils.V_ERR_KEYUSAGE_NO_CRL_SIGN;
                    ok = context.verifyCallback.call(context, ZERO);
                    if ( ok == 0 ) return ok;
                }
                final PublicKey ikey = issuer.getPublicKey();
                if ( ikey == null ) {
                    context.error = X509Utils.V_ERR_UNABLE_TO_DECODE_ISSUER_PUBLIC_KEY;
                    ok = context.verifyCallback.call(context, ZERO);
                    if ( ok == 0 ) return ok;
                }
                else {
                    try {
                        SecurityHelper.verify(crl, ikey);
                    }
                    catch (GeneralSecurityException ex) {
                        context.error = X509Utils.V_ERR_CRL_SIGNATURE_FAILURE;
                        ok = context.verifyCallback.call(context, ZERO);
                        if ( ok == 0 ) return ok;
                    }
                }
            }

            ok = context.checkCRLTime(crl, 1);
            if ( ok == 0 ) return ok;

            return 1;
        }
    };

    /**
     * c: cert_crl
     */
    final static Store.CertificateCRLFunction defaultCertificateCRL = new Store.CertificateCRLFunction() {
        public int call(final StoreContext context, final X509CRL crl, X509AuxCertificate x) throws Exception {
            int ok;
            if ( crl.getRevokedCertificate( x.getSerialNumber() ) != null ) {
                context.error = X509Utils.V_ERR_CERT_REVOKED;
                ok = context.verifyCallback.call(context, ZERO);
                if ( ok == 0 ) return 0;
            }
            if ( (context.verifyParameter.flags & X509Utils.V_FLAG_IGNORE_CRITICAL) != 0 ) {
                return 1;
            }
            if ( crl.getCriticalExtensionOIDs() != null && crl.getCriticalExtensionOIDs().size() > 0 ) {
                context.error = X509Utils.V_ERR_UNHANDLED_CRITICAL_CRL_EXTENSION;
                ok = context.verifyCallback.call(context, ZERO);
                if ( ok == 0 ) return 0;
            }
            return 1;
        }
    };

    /**
     * c: check_policy
     */
    final static CheckPolicyFunction defaultCheckPolicy = new CheckPolicyFunction() {
        public int call(StoreContext context) throws Exception {
            return 1;
        }
    };
}// X509_STORE_CTX
