/* WolfCryptPKIXCertPathValidator.java
 *
 * Copyright (C) 2006-2025 wolfSSL Inc.
 *
 * This file is part of wolfSSL.
 *
 * wolfSSL is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * wolfSSL 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
 */

package com.wolfssl.provider.jce;

import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.Collection;
import java.util.ArrayList;
import java.security.InvalidAlgorithmParameterException;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.security.cert.TrustAnchor;
import java.security.cert.CertPathValidatorSpi;
import java.security.cert.CertPathValidatorResult;
import java.security.cert.CertPath;
import java.security.cert.CertPathChecker;
import java.security.cert.CertPathParameters;
import java.security.cert.CertPathValidatorException;
import java.security.cert.CertSelector;
import java.security.cert.CertStore;
import java.security.cert.CertStoreException;
import java.security.cert.X509CertSelector;
import java.security.cert.PKIXParameters;
import java.security.cert.PKIXCertPathChecker;
import java.security.cert.PKIXCertPathValidatorResult;
import java.security.cert.CRL;
import java.security.cert.X509CRL;
import java.security.cert.X509CRLSelector;
import javax.security.auth.x500.X500Principal;

import com.wolfssl.wolfcrypt.Fips;
import com.wolfssl.wolfcrypt.WolfCrypt;
import com.wolfssl.wolfcrypt.WolfSSLCertManager;
import com.wolfssl.wolfcrypt.WolfCryptException;

/**
 * wolfJCE implementation of CertPathValidator for PKIX (X.509)
 *
 * This implementation supports most of CertPathValidator, but not the
 * following items. If needed, please contact support@wolfssl.com
 * with details of required support.
 *
 *     A. Certificate policies, and this related setters/getters. As such,
 *        validation will not return PolicyNode in CertPathValidatorResult
 *     B. Overriding current date for validation (PKIXParameters.setDate())
 *     C. getRevocationChecker() throws UnsupportedOperationException.
 *        Internal revocation is done with CRL if
 *        PKIXParameters.isRevocationEnabled() is true and appropriate CRLs
 *        have been loaded into CertStore Set
 */
public class WolfCryptPKIXCertPathValidator extends CertPathValidatorSpi {

    /**
     * Create new WolfCryptPKIXCertPathValidator object.
     */
    public WolfCryptPKIXCertPathValidator() {
        log("created new WolfCryptPKIXCertPathValidator");
    }

    /**
     * Check CertPathParameters matches our requirements.
     *    1. Not null
     *    2. Is an instance of PKIXParameters
     *
     * @throws InvalidAlgorithmParameterException if null or not an instance
     *         of PKIXParameters
     */
    private void sanitizeCertPathParameters(CertPathParameters params)
        throws InvalidAlgorithmParameterException {

        log("sanitizing CertPathParameters");

        if (params == null) {
            throw new InvalidAlgorithmParameterException(
                "CertPathParameters is null");
        }

        /* Check params is of type PKIXParameters */
        if (!(params instanceof PKIXParameters)) {
            throw new InvalidAlgorithmParameterException(
                "params not of type PKIXParameters");
        }
    }

    /**
     * Check CertPath matches our requirements.
     *   1. CertPath.getType() is "X.509"
     *   2. CertPath.getEncoding() contains "PkiPath"
     *
     * @throws InvalidAlgorithmParametersException if type is not X.509
     * @throws CertPathValidatorException if PkiPath encoding is not supported
     */
    private void sanitizeCertPath(CertPath path)
        throws InvalidAlgorithmParameterException, CertPathValidatorException {

        boolean pkiPathEncodingSupported = false;
        Iterator<String> supportedCertEncodings = null;

        log("sanitizing CertPath");

        /* Verify CertPath type is X.509 */
        if (!path.getType().equals("X.509")) {
            throw new InvalidAlgorithmParameterException(
                "PKIX CertPathValidator only supports X.509");
        }

        /* Check that PkiPath encoding is supported, which is an
         * ASN.1 DER encoded sequence of the cert */
        supportedCertEncodings = path.getEncodings();
        while (supportedCertEncodings.hasNext()) {
            if (supportedCertEncodings.next().equals("PkiPath")) {
                pkiPathEncodingSupported = true;
            }
        }
        if (!pkiPathEncodingSupported) {
            throw new CertPathValidatorException(
                "PkiPath CertPath encoding not supported but required");
        }
    }

    private void checkTargetCertConstraints(X509Certificate cert,
        int certIdx, CertPath path, PKIXParameters params)
        throws CertPathValidatorException {

        CertSelector selector = null;
        X509CertSelector x509Selector = null;

        if (cert == null || params == null) {
            throw new CertPathValidatorException(
                "X509Certificate in chain or PKIXParameters is null");
        }

        /* Only check leaf/peer certificate against constraints */
        if (certIdx != 0) {
            return;
        }

        /* Use CertSelector to check target cert */
        selector = params.getTargetCertConstraints();
        if (selector != null) {
            log("checking target cert constraints against CertSelector");

            if (!(selector instanceof X509CertSelector)) {
                throw new CertPathValidatorException(
                    "CertSelector not of type X509CertSelector");
            }
            x509Selector = (X509CertSelector)selector;

            if (!x509Selector.match(cert)) {
                throw new CertPathValidatorException(
                    "Target certificate did not pass CertConstraints check");
            }
        }
        else {
            log("no cert constraints in params, not checking CertSelector");
        }
    }

    private void disallowCertPolicyUse(PKIXParameters params)
        throws CertPathValidatorException {

        if (params == null) {
            throw new CertPathValidatorException(
                "PKIXParameters is null when checking for cert policies");
        }

        if (!params.getInitialPolicies().isEmpty()) {
            throw new CertPathValidatorException(
                "Certificate policies not supported by wolfJCE " +
                "CertPathValidator, PKIXParameters.getInitialPolicies() is " +
                "not empty");
        }

        /* Ignored, but log for debugging */
        log("PKIXParameters.getPolicyQualifiersRejected(): " +
            params.getPolicyQualifiersRejected());
        log("PKIXParameters.isPolicyMappingInhibited(): " +
            params.isPolicyMappingInhibited());

        /* Should the any policy OID be processed if it is included in
         * a certificate? Default is false, don't allow enablement since
         * not supported here yet */
        if (params.isAnyPolicyInhibited()) {
            throw new CertPathValidatorException(
                "Certificate policies not supported by wolfJCE " +
                "CertPathValidator. PKIXParameters.setAnyPolicyInhibited() " +
                "must be set to false (default)");
        }

        /* If true an acceptable policy needs to be explicitly identified in
         * every certificate. Default is false, don't allow enablement since
         * not supported here yet */
        if (params.isExplicitPolicyRequired()) {
            throw new CertPathValidatorException(
                "Certificate policies not supported by wolfJCE " +
                "CertPathValidator. PKIXParameters.setExplicitPolicy" +
                "Required() must be set to false (default)");
        }
    }

    /**
     * Check X509Certificate against constraints or settings inside
     * PKIXParameters.
     *
     * @param cert certificate to check
     * @param certIdx index of certificate, used when throwing exception
     * @param path CertPath used when throwing exception
     * @param params parameters used to get constraints from
     *
     * @throws CertPathValidatorException if checks on certificate fail
     */
    private void sanitizeX509Certificate(X509Certificate cert,
        int certIdx, CertPath path, PKIXParameters params)
        throws CertPathValidatorException {

        if (cert == null || params == null) {
            throw new CertPathValidatorException(
                "X509Certificate in chain or PKIXParameters is null");
        }

        /* Check target cert constraints, if set in parameters */
        checkTargetCertConstraints(cert, certIdx, path, params);

        /* Certificate policies are not currently supported by this
         * CertPathValidator implementation, throw exceptions when
         * user tries to use them. */
        disallowCertPolicyUse(params);
    }

    /**
     * Call all PKIXCertPathCheckers that have been registered into
     * PKIXParameters. This allows users to do additional verification
     * steps on certificates if needed.
     *
     * @param cert certificate to be checked
     * @param params parameters from which to get PKIXCertPathChecker list
     *
     * @throws CertPathValidatorException if a checker fails validation on
     *         the given Certificate
     */
    private void callCertPathCheckers(X509Certificate cert,
        PKIXParameters params) throws CertPathValidatorException {

        int i = 0;
        List<PKIXCertPathChecker> pathCheckers = null;

        if (cert == null || params == null) {
            throw new CertPathValidatorException(
                "X509Certificate in chain or PKIXParameters is null");
        }

        pathCheckers = params.getCertPathCheckers();
        if (pathCheckers == null) {
            /* Spec says this cannot be null */
            throw new CertPathValidatorException(
                "PKIXParameters.getCertPathCheckers() should not return null");
        }
        if (pathCheckers.isEmpty()) {
            return;
        }

        for (i = 0; i < pathCheckers.size(); i++) {
            log("calling CertPathChecker: " + pathCheckers.get(i));

            /* Throws CertPathValidatorException on error */
            pathCheckers.get(i).check((Certificate)cert);
        }
    }

    /**
     * Load TrustAnchors from PKIXParameters into WolfSSLCertManager as
     * trusted CA certificates.
     *
     * @param params PKIXParameters from which to get TrustAnchor Set
     * @param cm WolfSSLCertManager to load TrustAnchors into as trusted roots
     *
     * @throws CertPathValidatorException on failure to load trust anchors
     */
    private void loadTrustAnchorsIntoCertManager(
        PKIXParameters params, WolfSSLCertManager cm)
        throws CertPathValidatorException {

        Set<TrustAnchor> trustAnchors = null;
        Iterator<TrustAnchor> trustIterator = null;

        log("loading TrustAnchors into native WolfSSLCertManager");

        if (params == null || cm == null) {
            throw new CertPathValidatorException(
                "PKIXParameters or WolfSSLCertManager are null when loading " +
                "TrustAnchors");
        }

        /* Load trust anchors into CertManager from PKIXParameters */
        trustAnchors = params.getTrustAnchors();
        if (trustAnchors == null || trustAnchors.isEmpty()) {
            throw new CertPathValidatorException(
                "No TrustAnchors in PKIXParameters");
        }

        /* Iterate through TrustAnchors, load as CAs into CertManager */
        trustIterator = trustAnchors.iterator();
        while (trustIterator.hasNext()) {
            TrustAnchor anchor = trustIterator.next();
            X509Certificate anchorCert = anchor.getTrustedCert();
            if (anchorCert != null) {
                try {
                    cm.CertManagerLoadCA(anchorCert);

                    log("loaded TrustAnchor: " +
                        anchorCert.getSubjectX500Principal().getName());

                } catch (WolfCryptException e) {
                    throw new CertPathValidatorException(e);
                }
            }
        }
    }

    /**
     * Verify X509Certificate chain from top down, ending with peer/leaf
     * cert last.
     *
     */
    private void verifyCertChain(CertPath path, PKIXParameters params,
        List<X509Certificate> certs, WolfSSLCertManager cm)
        throws CertPathValidatorException {

        int i = 0;
        X509Certificate cert = null;

        if (path == null || params == null || certs == null || cm == null) {
            throw new CertPathValidatorException(
                "Input args to verifyCertChain are null");
        }

        log("verifying certificate chain (chain size: " + certs.size() + ")");

        /* Process certs from List in reverse order (top to peer) */
        for (i = certs.size()-1; i >= 0; i--) {
            cert = certs.get(i);

            try {
                /* Try to verify cert */
                cm.CertManagerVerify(cert);

                log("verified chain [" + i + "]: " +
                    cert.getSubjectX500Principal().getName());

            } catch (WolfCryptException e) {
                log("failed verification chain [" + i + "]: " +
                    cert.getSubjectX500Principal().getName());

                throw new CertPathValidatorException(
                    "Failed verification on certificate", e, path, i);
            }

            /* Verified successfully. If this is a CA and we have more
             * certs, load this as trusted (intermediate) */
            if (i > 0 && cert.getBasicConstraints() >= 0) {
                try {
                    cm.CertManagerLoadCA(cert);

                    log("chain [" + i + "] is intermediate, loading as root");

                } catch (WolfCryptException e) {

                    log("chain [" + i + "] is CA, but failed to load as " +
                        "trusted root, not loading");
                }
            }
        }
    }

    /**
     * Search TrustAnchors in PKIXParameters for one that verifies the provided
     * X509Certificate.
     *
     * @param params PKIXParameters to get TrustAnchors from
     * @param cert X509Certificate for which to find signer cert
     *
     * @return TrustAnchor that signs provided cert
     *
     * @throws CertPathValidatorException if the search for TrustAnchor fails
     */
    public TrustAnchor findTrustAnchor(PKIXParameters params,
        X509Certificate cert) throws CertPathValidatorException {

        Set<TrustAnchor> trustAnchors = null;
        Iterator<TrustAnchor> trustIterator = null;
        TrustAnchor anchorFound = null;
        X500Principal issuer = null;
        WolfSSLCertManager cm = null;

        if (params == null || cert == null) {
            throw new CertPathValidatorException(
                "Input parameters are null to findTrustAnchor");
        }

        /* Issuer name we need to match */
        issuer = cert.getIssuerX500Principal();
        if (issuer == null) {
            throw new CertPathValidatorException(
                "Unable to get expected issuer name");
        }

        /* Get all TrustAnchors in PKIXParameters */
        trustAnchors = params.getTrustAnchors();
        if (trustAnchors == null || trustAnchors.isEmpty()) {
            throw new CertPathValidatorException(
                "No TrustAnchors in PKIXParameters");
        }

        try {
            cm = new WolfSSLCertManager();
        } catch (WolfCryptException e) {
            throw new CertPathValidatorException(
                "Failed to create native WolfSSLCertManager");
        }

        /* Iterate through TrustAnchors and check for match */
        trustIterator = trustAnchors.iterator();
        while (trustIterator.hasNext()) {
            TrustAnchor anchor = trustIterator.next();
            X509Certificate anchorCert = anchor.getTrustedCert();
            if (anchorCert == null) {
                /* Skip to next */
                continue;
            }

            if (!anchorCert.getSubjectX500Principal().equals(issuer)) {
                /* Isser name doesn't match, skip to next */
                continue;
            }

            try {
                /* Unload any CAs in CertManager */
                cm.CertManagerUnloadCAs();
            } catch (WolfCryptException e) {
                cm.free();
                throw new CertPathValidatorException(
                    "Unable to unload CAs from native WolfSSLCertManager");
            }

            try {
                /* Load anchor as CA */
                cm.CertManagerLoadCA(anchorCert);
            } catch (WolfCryptException e) {
                /* error loading CA, skip to next */
                continue;
            }

            try {
                /* Try to verify cert, mark found if successful */
                cm.CertManagerVerify(cert);
                anchorFound = anchor;
            } catch (WolfCryptException e) {
                /* Does not verify, skip to next */
                continue;
            }
        }

        /* Free native WolfSSLCertManager resources */
        cm.free();

        return anchorFound;
    }

    /**
     * Check if revocation has been enabled in PKIXParameters, and if so
     * find and load any CRLs in params.getCertStores().
     *
     * @param params parameters used to check if revocation is enabled
     *        and if so load any CRLs available
     * @param cm WolfSSLCertManager to load CRLs into
     * @param targetCert peer/leaf cert used to find matching CRL
     *
     * @throws CertPathValidatorException if error is encountered during
     *        revocation checking or CRL loading
     */
    private void checkRevocationEnabledAndLoadCRLs(
        PKIXParameters params, WolfSSLCertManager cm,
        X509Certificate targetCert)
        throws CertPathValidatorException {

        int i = 0;
        int loadedCount = 0;
        List<CertStore> stores = null;
        Collection<? extends CRL> crls = null;

        if (params == null || cm == null) {
            throw new CertPathValidatorException(
                "PKIXParameters or WolfSSLCertManager is null");
        }

        if (params.isRevocationEnabled()) {
            log("revocation enabled in PKIXParameters, checking " +
                "for CRLs to load");

            if (!WolfCrypt.CrlEnabled()) {
                throw new CertPathValidatorException(
                    "Revocation enabled in PKIXParameters but native " +
                    "wolfCrypt CRL not compiled in");
            }

            /* Enable CRL in native WolfSSLCertManager */
            cm.CertManagerEnableCRL(WolfCrypt.WOLFSSL_CRL_CHECK);

            log("CRL support enabled in native WolfSSLCertManager");

            stores = params.getCertStores();
            if (stores == null || stores.isEmpty()) {
                log("no CertStores in PKIXParameters to load CRLs");
                return;
            }

            /* Create CRL selector to help match target X509Certificate */
            X509CRLSelector selector = new X509CRLSelector();
            selector.setCertificateChecking(targetCert);

            try {
                /* Find and load any matching CRLs */
                for (i = 0; i < stores.size(); i++) {
                    crls = stores.get(i).getCRLs(selector);
                    for (CRL crl: crls) {
                        if (crl instanceof X509CRL) {
                            cm.CertManagerLoadCRL((X509CRL)crl);
                            loadedCount++;
                        }
                    }
                }
            } catch (CertStoreException e) {
                throw new CertPathValidatorException(e);
            }

            log("loaded " + loadedCount + " CRLs into WolfSSLCertManager");
        }
        else {
            log("revocation not enabled in PKIXParameters");
        }
    }

    /**
     * Validates the specified certification path using the provided
     * algorithm parameter set.
     *
     * General validation process follows:
     *   1. Sanitize CertPathParameters
     *       a. Verify not null and instanceof PKIXParameters
     *   2. Sanitize CertPath
     *       a. CertPath.getType() is "X.509"
     *       b. CertPath.getEncoding() contains "PkiPath"
     *   3. If wolfCrypt FIPS, verify params.getSigProvider() is wolfJCE
     *   4. Sanitize Certificate objects in CertPath chain
     *       a. Check target certificate constraints meet target cert
     *       b. Check cert policies are not used (not supported)
     *   5. Call any registered CertPathCheckers
     *   6. Load TrustAnchors into WolfSSLCertManager
     *   7. Enable CRL if requested, load CRLs from getCertStores()
     *   8. Verify X.509 certificate chain
     *   9. Find top-most TrustAnchor for return object
     *
     * @param certPath the CertPath to be validated. CertPath entries are
     *                 ordered from leaf/peer up the chain to CA/root last.
     *                 The certificate representing the last/final TrustAnchor
     *                 should not be part of the CertPath.
     * @param params the algorithm parameters to be used for validation
     *
     * @return the result of the validation
     *
     * @throws CertPathValidatorException if the CertPath does not validate
     * @throws InvalidAlgorithmParameterException if the parameters or type
     *         specified are unsupported or inappropriate for this
     *         CertPathValidator implementation.
     */
    @Override
    public CertPathValidatorResult engineValidate(
        CertPath certPath, CertPathParameters params)
        throws CertPathValidatorException, InvalidAlgorithmParameterException {

        int i = 0;
        PKIXParameters pkixParams = null;
        List<X509Certificate> certs = null;
        WolfSSLCertManager cm = null;
        TrustAnchor trustAnchor = null;

        log("entered engineValidate(), FIPS enabled: " + Fips.enabled);

        sanitizeCertPathParameters(params);
        sanitizeCertPath(certPath);

        pkixParams = (PKIXParameters)params;

        /* If we are in FIPS mode, verify wolfJCE is the Signature provider
         * to help maintain FIPS compliance */
        if (Fips.enabled && pkixParams.getSigProvider() != "wolfJCE") {
            if (pkixParams.getSigProvider() == null) {
                /* Preferred Signature provider not set, set to wolfJCE */
                pkixParams.setSigProvider("wolfJCE");
            }
            else {
                throw new CertPathValidatorException(
                    "CertPathParameters Signature Provider must be wolfJCE " +
                    "when using wolfCrypt FIPS: " +
                    pkixParams.getSigProvider());
            }
        }

        /* Use wolfSSL CertManager to facilitate chain verification */
        try {
            cm = new WolfSSLCertManager();
        } catch (WolfCryptException e) {
            throw new CertPathValidatorException(
                "Failed to create native WolfSSLCertManager");
        }

        try {
            if (pkixParams.getDate() != null) {
                /* TODO: If pkixParams.getDate() is not null, we should
                 * use that time for verification instead of current time.
                 * Will need to wrap/register/use native time callback
                 * with wc_SetTimeCb() */
                throw new CertPathValidatorException(
                    "Overriding date not supported with wolfJCE " +
                    "CertPathValidator implementation yet");
            }

            /* Get List of Certificate objects in CertPath, sanity check
             * that they are X509Certificate instances. */
            certs = new ArrayList<>();
            for (Certificate cert : certPath.getCertificates()) {
                if (cert instanceof X509Certificate) {
                    certs.add((X509Certificate) cert);
                }
            }
            if (certs.size() == 0) {
                throw new CertPathValidatorException(
                    "No Certificate objects in CertPath");
            }

            /* Sanity checks on certs from PKIXParameters constraints */
            for (i = 0; i < certs.size(); i++) {
                sanitizeX509Certificate(certs.get(i), i, certPath, pkixParams);
                callCertPathCheckers(certs.get(i), pkixParams);
            }

            /* Load trust anchors into CertManager from PKIXParameters */
            loadTrustAnchorsIntoCertManager(pkixParams, cm);

            /* Enable CRL if PKIXParameters.isRevocationEnabled(), load
             * any CRLs found in PKIXParameters.getCertStores(). Needs to
             * happen after trust anchors are loaded, since native wolfSSL
             * will try to find/verify CRL against trusted roots on load */
            checkRevocationEnabledAndLoadCRLs(pkixParams, cm, certs.get(0));

            /* Verify cert chain */
            verifyCertChain(certPath, pkixParams, certs, cm);

            /* Cert chain has been verified, find TrustAnchor to return
             * in PKIXCertPathValidatorResult */
            trustAnchor = findTrustAnchor(
                pkixParams, certs.get(certs.size() - 1));

        } finally {
            /* Free native WolfSSLCertManager resources */
            cm.free();
        }

        /* PolicyNode not returned, since certificate policies are not
         * yet supported */
        return new PKIXCertPathValidatorResult(trustAnchor, null,
            certs.get(0).getPublicKey());
    }

    /**
     * Returns a CertPathChecker that this implementation uses to check the
     * revocation status of certificates.
     *
     * Not currently implemented in wolfJCE.
     *
     * @return a CertPathChecker object that this implementation uses to
     *         check the revocation status of certificates.
     * @throws UnsupportedOperationException if this method is not implemented
     */
    @Override
    public CertPathChecker engineGetRevocationChecker()
        throws UnsupportedOperationException {

        throw new UnsupportedOperationException(
            "getRevocationChecker() not supported by wolfJCE");
    }

    /**
     * Internal log function, called when debug is enabled.
     *
     * @param msg Log message to be printed
     */
    private void log(String msg) {
        WolfCryptDebug.log(getClass(), WolfCryptDebug.INFO, () -> msg);
    }
}

