org.xipki.ca.client.impl.CAClientImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.xipki.ca.client.impl.CAClientImpl.java

Source

/*
 *
 * This file is part of the XiPKI project.
 * Copyright (c) 2014 - 2015 Lijun Liao
 * Author: Lijun Liao
 *
 * 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
 * THE AUTHOR LIJUN LIAO. LIJUN LIAO 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/>.
 *
 * 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.
 *
 * 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 XiPKI software without
 * disclosing the source code of your own applications.
 *
 * For more information, please contact Lijun Liao at this
 * address: lijun.liao@gmail.com
 */

package org.xipki.ca.client.impl;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PublicKey;
import java.security.Security;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.CertificateException;
import java.security.cert.X509CRL;
import java.security.cert.X509Certificate;
import java.security.interfaces.ECPublicKey;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.validation.SchemaFactory;

import org.bouncycastle.asn1.cmp.CMPCertificate;
import org.bouncycastle.asn1.cmp.PKIFailureInfo;
import org.bouncycastle.asn1.cmp.PKIMessage;
import org.bouncycastle.asn1.cmp.PKIStatus;
import org.bouncycastle.asn1.crmf.CertRequest;
import org.bouncycastle.asn1.crmf.ProofOfPossession;
import org.bouncycastle.asn1.pkcs.CertificationRequest;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.Certificate;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jce.provider.X509CertificateObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xipki.ca.client.api.CAClient;
import org.xipki.ca.client.api.CAClientException;
import org.xipki.ca.client.api.CertIdOrError;
import org.xipki.ca.client.api.CertOrError;
import org.xipki.ca.client.api.CertprofileInfo;
import org.xipki.ca.client.api.EnrollCertResult;
import org.xipki.ca.client.api.PKIErrorException;
import org.xipki.ca.client.api.RemoveExpiredCertsResult;
import org.xipki.ca.client.api.dto.CRLResultType;
import org.xipki.ca.client.api.dto.EnrollCertRequestEntryType;
import org.xipki.ca.client.api.dto.EnrollCertRequestType;
import org.xipki.ca.client.api.dto.EnrollCertResultEntryType;
import org.xipki.ca.client.api.dto.EnrollCertResultType;
import org.xipki.ca.client.api.dto.ErrorResultEntryType;
import org.xipki.ca.client.api.dto.IssuerSerialEntryType;
import org.xipki.ca.client.api.dto.P10EnrollCertRequestType;
import org.xipki.ca.client.api.dto.ResultEntryType;
import org.xipki.ca.client.api.dto.RevokeCertRequestEntryType;
import org.xipki.ca.client.api.dto.RevokeCertRequestType;
import org.xipki.ca.client.api.dto.RevokeCertResultEntryType;
import org.xipki.ca.client.api.dto.RevokeCertResultType;
import org.xipki.ca.client.api.dto.UnrevokeOrRemoveCertRequestType;
import org.xipki.ca.client.impl.jaxb.CAClientType;
import org.xipki.ca.client.impl.jaxb.CAType;
import org.xipki.ca.client.impl.jaxb.CertprofileType;
import org.xipki.ca.client.impl.jaxb.CertprofilesType;
import org.xipki.ca.client.impl.jaxb.FileOrValueType;
import org.xipki.ca.client.impl.jaxb.ObjectFactory;
import org.xipki.ca.client.impl.jaxb.RequestorType;
import org.xipki.ca.client.impl.jaxb.ResponderType;
import org.xipki.common.ConfigurationException;
import org.xipki.common.HealthCheckResult;
import org.xipki.common.ParamChecker;
import org.xipki.common.RequestResponseDebug;
import org.xipki.common.util.CollectionUtil;
import org.xipki.common.util.IoUtil;
import org.xipki.common.util.LogUtil;
import org.xipki.common.util.X509Util;
import org.xipki.common.util.XMLUtil;
import org.xipki.security.api.ConcurrentContentSigner;
import org.xipki.security.api.SecurityFactory;
import org.xipki.security.api.SignerException;
import org.xml.sax.SAXException;

/**
 * @author Lijun Liao
 */

public final class CAClientImpl implements CAClient {
    private class ClientConfigUpdater implements Runnable {
        private static final long MINUTE = 60L * 1000;
        private AtomicBoolean inProcess = new AtomicBoolean(false);
        private long lastUpdate;

        ClientConfigUpdater() {
        }

        @Override
        public void run() {
            if (inProcess.get()) {
                return;
            }

            inProcess.set(true);

            try {
                // just updated within the last 2 minutes
                if (System.currentTimeMillis() - lastUpdate < 2 * MINUTE) {
                    return;
                }

                autoConfCAs(null);
            } finally {
                lastUpdate = System.currentTimeMillis();
                inProcess.set(false);
            }
        }
    }

    private static final Logger LOG = LoggerFactory.getLogger(CAClientImpl.class);

    private static Object jaxbUnmarshallerLock = new Object();
    private static Unmarshaller jaxbUnmarshaller;

    private final Map<String, CAConf> casMap = new HashMap<>();

    private SecurityFactory securityFactory;

    private String confFile;
    private Map<X509Certificate, Boolean> tryXipkiNSStoVerifyMap = new ConcurrentHashMap<>();
    private ScheduledThreadPoolExecutor scheduledThreadPoolExecutor;

    public CAClientImpl() {
    }

    public void setSecurityFactory(final SecurityFactory securityFactory) {
        this.securityFactory = securityFactory;
    }

    /**
     *
     * @return names of CAs which could not been configured
     */
    private Set<String> autoConfCAs(final Set<String> caNamesToBeConfigured) {
        Set<String> caNamesWithError = new HashSet<>();

        Set<String> errorCANames = new HashSet<>();
        for (String name : casMap.keySet()) {
            if (caNamesToBeConfigured != null && caNamesToBeConfigured.contains(name) == false) {
                continue;
            }

            CAConf ca = casMap.get(name);

            if (ca.isCertAutoconf() == false && ca.isCertprofilesAutoconf() == false) {
                continue;
            }

            try {
                CAInfo caInfo = ca.getRequestor().retrieveCAInfo(name, null);
                if (ca.isCertAutoconf()) {
                    ca.setCert(caInfo.getCert());
                }
                if (ca.isCertprofilesAutoconf()) {
                    ca.setCertprofiles(caInfo.getCertprofiles());
                }
                LOG.info("retrieved CAInfo for CA " + name);
            } catch (Throwable t) {
                errorCANames.add(name);
                caNamesWithError.add(name);
                final String message = "could not retrieve CAInfo for CA " + name;
                if (LOG.isWarnEnabled()) {
                    LOG.warn(LogUtil.buildExceptionLogFormat(message), t.getClass().getName(), t.getMessage());
                }
                LOG.debug(message, t);
            }
        }

        if (CollectionUtil.isNotEmpty(errorCANames)) {
            for (String caName : errorCANames) {
                casMap.remove(caName);
            }
        }

        return caNamesWithError;
    }

    public void init() throws ConfigurationException, IOException {
        ParamChecker.assertNotNull("confFile", confFile);
        ParamChecker.assertNotNull("securityFactory", securityFactory);

        if (Security.getProvider("BC") == null) {
            Security.addProvider(new BouncyCastleProvider());
        }

        File configFile = new File(IoUtil.expandFilepath(confFile));
        if (configFile.exists() == false) {
            throw new FileNotFoundException("cound not find configuration file " + confFile);
        }

        CAClientType config = parse(new FileInputStream(configFile));
        int numActiveCAs = 0;

        for (CAType caType : config.getCAs().getCA()) {
            if (caType.isEnabled() == false) {
                LOG.info("CA " + caType.getName() + " is disabled");
                continue;
            }
            numActiveCAs++;
        }

        if (numActiveCAs == 0) {
            LOG.warn("no active CA is configured");
        }

        Boolean b = config.isDevMode();
        boolean devMode = b != null && b.booleanValue();

        // responders
        Map<String, X509Certificate> responders = new HashMap<>();
        for (ResponderType m : config.getResponders().getResponder()) {
            X509Certificate cert;
            try {
                cert = X509Util.parseCert(readData(m.getCert()));
            } catch (CertificateException e) {
                final String message = "could not configure responder " + m.getName();
                if (LOG.isWarnEnabled()) {
                    LOG.warn(LogUtil.buildExceptionLogFormat(message), e.getClass().getName(), e.getMessage());
                }
                LOG.debug(message, e);

                throw new ConfigurationException(e.getMessage(), e);
            }
            responders.put(m.getName(), cert);
        }

        // CA
        Set<String> configuredCaNames = new HashSet<>();

        Set<CAConf> cas = new HashSet<>();
        for (CAType caType : config.getCAs().getCA()) {
            b = caType.isEnabled();
            if (b.booleanValue() == false) {
                continue;
            }

            String caName = caType.getName();
            try {
                // responder
                X509Certificate responder = responders.get(caType.getResponder());
                if (responder == null) {
                    throw new ConfigurationException(
                            "no responder named " + caType.getResponder() + " is configured");
                }
                CAConf ca = new CAConf(caName, caType.getUrl(), caType.getHealthUrl(), caType.getRequestor(),
                        responder);

                // CA cert
                if (caType.getCaCert().getAutoconf() != null) {
                    ca.setCertAutoconf(true);
                } else {
                    ca.setCertAutoconf(true);
                    ca.setCert(X509Util.parseCert(readData(caType.getCaCert().getCert())));
                }

                // Certprofiles
                CertprofilesType certprofilesType = caType.getCertprofiles();
                if (certprofilesType.getAutoconf() != null) {
                    ca.setCertprofilesAutoconf(true);
                } else {
                    ca.setCertprofilesAutoconf(false);

                    List<CertprofileType> types = certprofilesType.getCertprofile();
                    Set<CertprofileInfo> profiles = new HashSet<>(types.size());
                    for (CertprofileType m : types) {
                        String conf = null;
                        if (m.getConf() != null) {
                            conf = m.getConf().getValue();
                            if (conf == null) {
                                conf = new String(IoUtil.read(m.getConf().getFile()));
                            }
                        }
                        CertprofileInfo profile = new CertprofileInfo(m.getName(), m.getType(), conf);
                        profiles.add(profile);
                    }
                    ca.setCertprofiles(profiles);
                }

                cas.add(ca);
                configuredCaNames.add(caName);
            } catch (IOException | CertificateException e) {
                final String message = "could not configure CA " + caName;
                if (LOG.isWarnEnabled()) {
                    LOG.warn(LogUtil.buildExceptionLogFormat(message), e.getClass().getName(), e.getMessage());
                }
                LOG.debug(message, e);

                if (devMode == false) {
                    throw new ConfigurationException(e.getMessage(), e);
                }
            }
        }

        // requestors
        Map<String, X509Certificate> requestorCerts = new HashMap<>();
        Map<String, ConcurrentContentSigner> requestorSigners = new HashMap<>();
        Map<String, Boolean> requestorSignRequests = new HashMap<>();

        for (RequestorType requestorConf : config.getRequestors().getRequestor()) {
            String name = requestorConf.getName();
            requestorSignRequests.put(name, requestorConf.isSignRequest());

            X509Certificate requestorCert = null;
            if (requestorConf.getCert() != null) {
                try {
                    requestorCert = X509Util.parseCert(readData(requestorConf.getCert()));
                    requestorCerts.put(name, requestorCert);
                } catch (Exception e) {
                    throw new ConfigurationException(e.getMessage(), e);
                }
            }

            // ------------------------------------------------
            if (requestorConf.getSignerType() != null) {
                try {
                    ConcurrentContentSigner requestorSigner = securityFactory.createSigner(
                            requestorConf.getSignerType(), requestorConf.getSignerConf(), requestorCert);
                    requestorSigners.put(name, requestorSigner);
                } catch (SignerException e) {
                    throw new ConfigurationException(e.getMessage(), e);
                }
            } else {
                if (requestorConf.isSignRequest()) {
                    throw new ConfigurationException("signer of requestor must be configured");
                } else if (requestorCert == null) {
                    throw new ConfigurationException(
                            "at least one of certificate and signer of requestor must be configured");
                }
            }
        }

        boolean autoConf = false;
        for (CAConf ca : cas) {
            if (this.casMap.containsKey(ca.getName())) {
                throw new ConfigurationException("duplicate CAs with the same name " + ca.getName());
            }

            if (ca.isCertAutoconf() || ca.isCertprofilesAutoconf()) {
                autoConf = true;
            }

            String requestorName = ca.getRequestorName();

            X509CmpRequestor cmpRequestor;
            if (requestorSigners.containsKey(requestorName)) {
                cmpRequestor = new DefaultHttpX509CmpRequestor(requestorSigners.get(requestorName),
                        ca.getResponder(), ca.getUrl(), securityFactory, requestorSignRequests.get(requestorName));
            } else if (requestorCerts.containsKey(requestorName)) {
                cmpRequestor = new DefaultHttpX509CmpRequestor(requestorCerts.get(requestorName), ca.getResponder(),
                        ca.getUrl(), securityFactory);
            } else {
                throw new ConfigurationException(
                        "could not find requestor named " + requestorName + " for CA " + ca.getName());
            }

            ca.setRequestor(cmpRequestor);
            this.casMap.put(ca.getName(), ca);
        }

        if (autoConf) {
            Integer cAInfoUpdateInterval = config.getCAs().getCAInfoUpdateInterval();
            if (cAInfoUpdateInterval == null) {
                cAInfoUpdateInterval = 10;
            } else if (cAInfoUpdateInterval <= 0) {
                cAInfoUpdateInterval = 0;
            } else if (cAInfoUpdateInterval < 5) {
                cAInfoUpdateInterval = 5;
            }

            Set<String> caNames = casMap.keySet();
            StringBuilder sb = new StringBuilder("configuring CAs ");
            sb.append(caNames);

            LOG.info(sb.toString());
            caNames = autoConfCAs(caNames);

            if (CollectionUtil.isNotEmpty(caNames)) {
                final String msg = "could not configured following CAs " + caNames;
                if (devMode) {
                    LOG.warn(msg);
                } else {
                    throw new ConfigurationException(msg);
                }
            }

            if (cAInfoUpdateInterval > 0) {
                scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(1);
                scheduledThreadPoolExecutor.scheduleAtFixedRate(new ClientConfigUpdater(), cAInfoUpdateInterval,
                        cAInfoUpdateInterval, TimeUnit.MINUTES);
            }
        }
    }

    public void shutdown() {
        if (scheduledThreadPoolExecutor != null) {
            scheduledThreadPoolExecutor.shutdown();
            while (scheduledThreadPoolExecutor.isTerminated() == false) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                }
            }
            scheduledThreadPoolExecutor = null;
        }
    }

    private static byte[] readData(final FileOrValueType fileOrValue) throws IOException {
        byte[] data = fileOrValue.getValue();
        if (data == null) {
            data = IoUtil.read(fileOrValue.getFile());
        }
        return data;
    }

    @Override
    public EnrollCertResult requestCert(final CertificationRequest p10Request, final String profile, String caName,
            final String username, final RequestResponseDebug debug) throws CAClientException, PKIErrorException {
        if (caName == null) {
            caName = getCANameForProfile(profile);
            if (caName == null) {
                throw new CAClientException("cert profile " + profile + " is not supported by any CA");
            }
        }

        CAConf ca = casMap.get(caName);
        if (ca == null) {
            throw new CAClientException("could not find CA named " + caName);
        }

        final String id = "cert-1";
        P10EnrollCertRequestType request = new P10EnrollCertRequestType(id, profile, p10Request);
        EnrollCertResultType result;
        try {
            result = ca.getRequestor().requestCertificate(request, username, debug);
        } catch (CmpRequestorException e) {
            throw new CAClientException(e.getMessage(), e);
        }

        return parseEnrollCertResult((EnrollCertResultType) result, caName);
    }

    @Override
    public EnrollCertResult requestCerts(final EnrollCertRequestType request, String caName, final String username,
            final RequestResponseDebug debug) throws CAClientException, PKIErrorException {
        ParamChecker.assertNotNull("request", request);

        List<EnrollCertRequestEntryType> requestEntries = request.getRequestEntries();
        if (CollectionUtil.isEmpty(requestEntries)) {
            return null;
        }

        boolean b = (caName != null);
        if (caName == null) {
            // detect the CA name
            String profile = requestEntries.get(0).getCertprofile();
            caName = getCANameForProfile(profile);
            if (caName == null) {
                throw new CAClientException("cert profile " + profile + " is not supported by any CA");
            }
        }

        if (b || request.getRequestEntries().size() > 1) {
            // make sure that all requests are targeted on the same CA
            for (EnrollCertRequestEntryType entry : request.getRequestEntries()) {
                String profile = entry.getCertprofile();
                checkCertprofileSupportInCA(profile, caName);
            }
        }

        CAConf ca = casMap.get(caName);
        if (ca == null) {
            throw new CAClientException("could not find CA named " + caName);
        }

        EnrollCertResultType result;
        try {
            result = ca.getRequestor().requestCertificate(request, username, debug);
        } catch (CmpRequestorException e) {
            throw new CAClientException(e.getMessage(), e);
        }

        return parseEnrollCertResult((EnrollCertResultType) result, caName);
    }

    private void checkCertprofileSupportInCA(final String certprofile, String caName) throws CAClientException {
        if (caName != null) {
            if (casMap.containsKey(caName) == false) {
                throw new CAClientException("unknown ca: " + caName);
            } else {
                CAConf ca = casMap.get(caName);
                if (ca.supportsProfile(certprofile) == false) {
                    throw new CAClientException(
                            "cert profile " + certprofile + " is not supported by the CA " + caName);
                }
            }
            return;
        }

        for (CAConf ca : casMap.values()) {
            if (ca.isCAInfoConfigured() == false) {
                continue;
            }
            if (ca.supportsProfile(certprofile)) {
                if (caName == null) {
                    caName = ca.getName();
                } else {
                    throw new CAClientException("cert profile " + certprofile
                            + " supported by more than one CA, please specify the CA name.");
                }
            }
        }

        if (caName == null) {
            throw new CAClientException("unsupported cert profile " + certprofile);
        }
    }

    @Override
    public CertIdOrError revokeCert(final X509Certificate cert, final int reason, final Date invalidityDate,
            final RequestResponseDebug debug) throws CAClientException, PKIErrorException {
        X500Name issuer = X500Name.getInstance(cert.getIssuerX500Principal().getEncoded());
        return revokeCert(issuer, cert.getSerialNumber(), reason, invalidityDate, debug);
    }

    @Override
    public CertIdOrError revokeCert(final X500Name issuer, final BigInteger serial, final int reason,
            final Date invalidityDate, final RequestResponseDebug debug)
            throws CAClientException, PKIErrorException {
        final String id = "cert-1";
        RevokeCertRequestEntryType entry = new RevokeCertRequestEntryType(id, issuer, serial, reason,
                invalidityDate);
        RevokeCertRequestType request = new RevokeCertRequestType();
        request.addRequestEntry(entry);
        Map<String, CertIdOrError> result = revokeCerts(request, debug);
        return result == null ? null : result.get(id);
    }

    @Override
    public Map<String, CertIdOrError> revokeCerts(final RevokeCertRequestType request,
            final RequestResponseDebug debug) throws CAClientException, PKIErrorException {
        ParamChecker.assertNotNull("request", request);

        List<RevokeCertRequestEntryType> requestEntries = request.getRequestEntries();
        if (CollectionUtil.isEmpty(requestEntries)) {
            return Collections.emptyMap();
        }

        X500Name issuer = requestEntries.get(0).getIssuer();
        for (int i = 1; i < requestEntries.size(); i++) {
            if (issuer.equals(requestEntries.get(i).getIssuer()) == false) {
                throw new PKIErrorException(PKIStatus.REJECTION, PKIFailureInfo.badRequest,
                        "revoking certificates issued by more than one CA is not allowed");
            }
        }

        final String caName = getCaNameByIssuer(issuer);
        X509CmpRequestor cmpRequestor = casMap.get(caName).getRequestor();
        RevokeCertResultType result;
        try {
            result = cmpRequestor.revokeCertificate(request, debug);
        } catch (CmpRequestorException e) {
            throw new CAClientException(e.getMessage(), e);
        }

        return parseRevokeCertResult(result);
    }

    private Map<String, CertIdOrError> parseRevokeCertResult(final RevokeCertResultType result)
            throws CAClientException {
        Map<String, CertIdOrError> ret = new HashMap<>();

        RevokeCertResultType _result = (RevokeCertResultType) result;
        for (ResultEntryType _entry : _result.getResultEntries()) {
            CertIdOrError certIdOrError;
            if (_entry instanceof RevokeCertResultEntryType) {
                RevokeCertResultEntryType entry = (RevokeCertResultEntryType) _entry;
                certIdOrError = new CertIdOrError(entry.getCertId());
            } else if (_entry instanceof ErrorResultEntryType) {
                ErrorResultEntryType entry = (ErrorResultEntryType) _entry;
                certIdOrError = new CertIdOrError(entry.getStatusInfo());
            } else {
                throw new CAClientException("unknwon type " + _entry);
            }

            ret.put(_entry.getId(), certIdOrError);
        }

        return ret;
    }

    @Override
    public X509CRL downloadCRL(final String caName, final RequestResponseDebug debug)
            throws CAClientException, PKIErrorException {
        return downloadCRL(caName, (BigInteger) null, debug);
    }

    @Override
    public X509CRL downloadCRL(final String caName, final BigInteger crlNumber, final RequestResponseDebug debug)
            throws CAClientException, PKIErrorException {
        ParamChecker.assertNotNull("caName", caName);

        if (casMap.containsKey(caName) == false) {
            throw new IllegalArgumentException("unknown CAConf " + caName);
        }

        X509CmpRequestor requestor = casMap.get(caName).getRequestor();
        CRLResultType result;
        try {
            if (crlNumber == null) {
                result = requestor.downloadCurrentCRL(debug);
            } else {
                result = requestor.downloadCRL(crlNumber, debug);
            }
        } catch (CmpRequestorException e) {
            throw new CAClientException(e.getMessage(), e);
        }

        return result.getCRL();
    }

    @Override
    public X509CRL generateCRL(final String caName, final RequestResponseDebug debug)
            throws CAClientException, PKIErrorException {
        ParamChecker.assertNotNull("caName", caName);

        if (casMap.containsKey(caName) == false) {
            throw new IllegalArgumentException("unknown CAConf " + caName);
        }

        X509CmpRequestor requestor = casMap.get(caName).getRequestor();
        try {
            CRLResultType result = requestor.generateCRL(debug);
            return result.getCRL();
        } catch (CmpRequestorException e) {
            throw new CAClientException(e.getMessage(), e);
        }
    }

    @Override
    public String getCaNameByIssuer(final X500Name issuer) throws CAClientException {

        if (issuer == null) {
            throw new CAClientException("invalid issuer");
        }

        for (String name : casMap.keySet()) {
            final CAConf ca = casMap.get(name);
            if (ca.isCAInfoConfigured() == false) {
                continue;
            }

            if (ca.getSubject().equals(issuer)) {
                return name;
            }
        }

        throw new CAClientException("unknown CA for issuer: " + issuer);
    }

    private String getCANameForProfile(final String certprofile) throws CAClientException {
        String caName = null;
        for (CAConf ca : casMap.values()) {
            if (ca.isCAInfoConfigured() == false) {
                continue;
            }

            if (ca.supportsProfile(certprofile)) {
                if (caName == null) {
                    caName = ca.getName();
                } else {
                    throw new CAClientException("cert profile " + certprofile
                            + " supported by more than one CA, please specify the CA name.");
                }
            }
        }

        return caName;
    }

    private java.security.cert.Certificate getCertificate(final CMPCertificate cmpCert)
            throws CertificateException {
        Certificate bcCert = cmpCert.getX509v3PKCert();
        return (bcCert == null) ? null : new X509CertificateObject(bcCert);
    }

    public String getConfFile() {
        return confFile;
    }

    public void setConfFile(String confFile) {
        this.confFile = confFile;
    }

    @Override
    public Set<String> getCaNames() {
        return casMap.keySet();
    }

    @Override
    public byte[] envelope(final CertRequest certRequest, final ProofOfPossession pop, final String profileName,
            String caName, final String username) throws CAClientException {
        if (caName == null) {
            // detect the CA name
            caName = getCANameForProfile(profileName);
            if (caName == null) {
                throw new CAClientException("cert profile " + profileName + " is not supported by any CA");
            }
        } else {
            checkCertprofileSupportInCA(profileName, caName);
        }

        CAConf ca = casMap.get(caName);
        if (ca == null) {
            throw new CAClientException("could not find CA named " + caName);
        }

        PKIMessage pkiMessage;
        try {
            pkiMessage = ca.getRequestor().envelope(certRequest, pop, profileName, username);
        } catch (CmpRequestorException e) {
            throw new CAClientException("CmpRequestorException: " + e.getMessage(), e);
        }

        try {
            return pkiMessage.getEncoded();
        } catch (IOException e) {
            throw new CAClientException("IOException: " + e.getMessage(), e);
        }
    }

    private boolean verify(final java.security.cert.Certificate caCert, final java.security.cert.Certificate cert) {
        if (caCert instanceof X509Certificate == false) {
            return false;
        }
        if (cert instanceof X509Certificate == false) {
            return false;
        }

        X509Certificate _caCert = (X509Certificate) caCert;
        X509Certificate _cert = (X509Certificate) cert;

        if (_cert.getIssuerX500Principal().equals(_caCert.getSubjectX500Principal()) == false) {
            return false;
        }

        boolean inLoadTest = Boolean.getBoolean("org.xipki.loadtest");
        if (inLoadTest) {
            return true;
        }

        final String provider = "XipkiNSS";
        Boolean tryXipkiNSStoVerify = tryXipkiNSStoVerifyMap.get(_caCert);
        PublicKey caPublicKey = _caCert.getPublicKey();
        try {
            if (tryXipkiNSStoVerify == null) {
                if (caPublicKey instanceof ECPublicKey || Security.getProvider(provider) == null) {
                    tryXipkiNSStoVerify = Boolean.FALSE;
                    tryXipkiNSStoVerifyMap.put(_caCert, tryXipkiNSStoVerify);
                } else {
                    byte[] tbs = _cert.getTBSCertificate();
                    byte[] signatureValue = _cert.getSignature();
                    String sigAlgName = _cert.getSigAlgName();
                    try {
                        Signature verifier = Signature.getInstance(sigAlgName, provider);
                        verifier.initVerify(caPublicKey);
                        verifier.update(tbs);
                        boolean sigValid = verifier.verify(signatureValue);

                        LOG.info("use {} to verify {} signature", provider, sigAlgName);
                        tryXipkiNSStoVerify = Boolean.TRUE;
                        tryXipkiNSStoVerifyMap.put(_caCert, tryXipkiNSStoVerify);
                        return sigValid;
                    } catch (Exception e) {
                        LOG.info("could not use {} to verify {} signature", provider, sigAlgName);
                        tryXipkiNSStoVerify = Boolean.FALSE;
                        tryXipkiNSStoVerifyMap.put(_caCert, tryXipkiNSStoVerify);
                    }
                }
            }

            if (tryXipkiNSStoVerify) {
                byte[] tbs = _cert.getTBSCertificate();
                byte[] signatureValue = _cert.getSignature();
                String sigAlgName = _cert.getSigAlgName();
                Signature verifier = Signature.getInstance(sigAlgName, provider);
                verifier.initVerify(caPublicKey);
                verifier.update(tbs);
                return verifier.verify(signatureValue);
            } else {
                _cert.verify(caPublicKey);
                return true;
            }
        } catch (SignatureException | InvalidKeyException | CertificateException | NoSuchAlgorithmException
                | NoSuchProviderException e) {
            LOG.debug("{} while verifying signature: {}", e.getClass().getName(), e.getMessage());
            return false;
        }
    }

    @Override
    public byte[] envelopeRevocation(final X500Name issuer, final BigInteger serial, final int reason)
            throws CAClientException {
        final String id = "cert-1";
        RevokeCertRequestEntryType entry = new RevokeCertRequestEntryType(id, issuer, serial, reason, null);
        RevokeCertRequestType request = new RevokeCertRequestType();
        request.addRequestEntry(entry);

        String caName = getCaNameByIssuer(issuer);
        X509CmpRequestor cmpRequestor = casMap.get(caName).getRequestor();

        try {
            PKIMessage pkiMessage = cmpRequestor.envelopeRevocation(request);
            return pkiMessage.getEncoded();
        } catch (CmpRequestorException | IOException e) {
            throw new CAClientException(e.getMessage(), e);
        }
    }

    @Override
    public byte[] envelopeRevocation(final X509Certificate cert, final int reason) throws CAClientException {
        X500Name issuer = X500Name.getInstance(cert.getIssuerX500Principal().getEncoded());
        return envelopeRevocation(issuer, cert.getSerialNumber(), reason);
    }

    @Override
    public CertIdOrError unrevokeCert(final X500Name issuer, final BigInteger serial,
            final RequestResponseDebug debug) throws CAClientException, PKIErrorException {
        final String id = "cert-1";
        IssuerSerialEntryType entry = new IssuerSerialEntryType(id, issuer, serial);
        UnrevokeOrRemoveCertRequestType request = new UnrevokeOrRemoveCertRequestType();
        request.addRequestEntry(entry);
        Map<String, CertIdOrError> result = unrevokeCerts(request, debug);
        return result == null ? null : result.get(id);
    }

    @Override
    public CertIdOrError unrevokeCert(final X509Certificate cert, final RequestResponseDebug debug)
            throws CAClientException, PKIErrorException {
        X500Name issuer = X500Name.getInstance(cert.getIssuerX500Principal().getEncoded());
        return unrevokeCert(issuer, cert.getSerialNumber(), debug);
    }

    @Override
    public Map<String, CertIdOrError> unrevokeCerts(final UnrevokeOrRemoveCertRequestType request,
            final RequestResponseDebug debug) throws CAClientException, PKIErrorException {
        ParamChecker.assertNotNull("request", request);

        List<IssuerSerialEntryType> requestEntries = request.getRequestEntries();
        if (CollectionUtil.isEmpty(requestEntries)) {
            return Collections.emptyMap();
        }

        X500Name issuer = requestEntries.get(0).getIssuer();
        for (int i = 1; i < requestEntries.size(); i++) {
            if (issuer.equals(requestEntries.get(i).getIssuer()) == false) {
                throw new PKIErrorException(PKIStatus.REJECTION, PKIFailureInfo.badRequest,
                        "unrevoking certificates issued by more than one CA is not allowed");
            }
        }

        final String caName = getCaNameByIssuer(issuer);
        X509CmpRequestor cmpRequestor = casMap.get(caName).getRequestor();
        RevokeCertResultType result;
        try {
            result = cmpRequestor.unrevokeCertificate(request, debug);
        } catch (CmpRequestorException e) {
            throw new CAClientException(e.getMessage(), e);
        }

        return parseRevokeCertResult(result);
    }

    @Override
    public CertIdOrError removeCert(final X500Name issuer, final BigInteger serial,
            final RequestResponseDebug debug) throws CAClientException, PKIErrorException {
        final String id = "cert-1";
        IssuerSerialEntryType entry = new IssuerSerialEntryType(id, issuer, serial);
        UnrevokeOrRemoveCertRequestType request = new UnrevokeOrRemoveCertRequestType();
        request.addRequestEntry(entry);
        Map<String, CertIdOrError> result = removeCerts(request, debug);
        return result == null ? null : result.get(id);
    }

    @Override
    public CertIdOrError removeCert(final X509Certificate cert, final RequestResponseDebug debug)
            throws CAClientException, PKIErrorException {
        X500Name issuer = X500Name.getInstance(cert.getIssuerX500Principal().getEncoded());
        return removeCert(issuer, cert.getSerialNumber(), debug);
    }

    @Override
    public Map<String, CertIdOrError> removeCerts(final UnrevokeOrRemoveCertRequestType request,
            final RequestResponseDebug debug) throws CAClientException, PKIErrorException {
        ParamChecker.assertNotNull("request", request);

        List<IssuerSerialEntryType> requestEntries = request.getRequestEntries();
        if (CollectionUtil.isEmpty(requestEntries)) {
            return Collections.emptyMap();
        }

        X500Name issuer = requestEntries.get(0).getIssuer();
        for (int i = 1; i < requestEntries.size(); i++) {
            if (issuer.equals(requestEntries.get(i).getIssuer()) == false) {
                throw new PKIErrorException(PKIStatus.REJECTION, PKIFailureInfo.badRequest,
                        "removing certificates issued by more than one CA is not allowed");
            }
        }

        final String caName = getCaNameByIssuer(issuer);
        X509CmpRequestor cmpRequestor = casMap.get(caName).getRequestor();
        RevokeCertResultType result;
        try {
            result = cmpRequestor.removeCertificate(request, debug);
        } catch (CmpRequestorException e) {
            throw new CAClientException(e.getMessage(), e);
        }

        return parseRevokeCertResult(result);
    }

    @Override
    public Set<CertprofileInfo> getCertprofiles(final String caName) {
        CAConf ca = casMap.get(caName);
        if (ca == null) {
            return Collections.emptySet();
        }

        Set<String> profileNames = ca.getProfileNames();
        if (CollectionUtil.isEmpty(profileNames)) {
            return Collections.emptySet();
        }

        Set<CertprofileInfo> ret = new HashSet<>(profileNames.size());
        for (String m : profileNames) {
            ret.add(ca.getProfile(m));
        }
        return ret;
    }

    @Override
    public RemoveExpiredCertsResult removeExpiredCerts(final String caName, final String certprofile,
            final String userLike, final long overlapSeconds, final RequestResponseDebug debug)
            throws CAClientException, PKIErrorException {
        ParamChecker.assertNotNull("caName", caName);

        if (casMap.containsKey(caName) == false) {
            throw new IllegalArgumentException("unknown CAConf " + caName);
        }

        X509CmpRequestor requestor = casMap.get(caName).getRequestor();
        try {
            return requestor.removeExpiredCerts(certprofile, userLike, overlapSeconds, debug);
        } catch (CmpRequestorException e) {
            throw new CAClientException(e.getMessage(), e);
        }
    }

    @Override
    public HealthCheckResult getHealthCheckResult(final String caName) throws CAClientException {
        ParamChecker.assertNotNull("caName", caName);

        if (casMap.containsKey(caName) == false) {
            throw new IllegalArgumentException("unknown CAConf " + caName);
        }

        String healthUrlStr = casMap.get(caName).getHealthUrl();

        URL serverUrl;
        try {
            serverUrl = new URL(healthUrlStr);
        } catch (MalformedURLException e) {
            throw new CAClientException("invalid URL '" + healthUrlStr + "'");
        }

        String name = "X509CA";
        HealthCheckResult healthCheckResult = new HealthCheckResult(name);

        try {
            HttpURLConnection httpUrlConnection = (HttpURLConnection) serverUrl.openConnection();
            InputStream inputStream = httpUrlConnection.getInputStream();
            int responseCode = httpUrlConnection.getResponseCode();
            if (responseCode != HttpURLConnection.HTTP_OK
                    && responseCode != HttpURLConnection.HTTP_INTERNAL_ERROR) {
                inputStream.close();
                throw new IOException("bad response: " + httpUrlConnection.getResponseCode() + "  "
                        + httpUrlConnection.getResponseMessage());
            }

            String responseContentType = httpUrlConnection.getContentType();
            boolean isValidContentType = false;
            if (responseContentType != null) {
                if (responseContentType.equalsIgnoreCase("application/json")) {
                    isValidContentType = true;
                }
            }
            if (isValidContentType == false) {
                inputStream.close();
                throw new IOException("bad response: mime type " + responseContentType + " not supported!");
            }

            byte[] responseBytes = IoUtil.read(inputStream);
            if (responseBytes.length == 0) {
                healthCheckResult.setHealthy(responseCode == HttpURLConnection.HTTP_OK);
            } else {
                String response = new String(responseBytes);
                try {
                    healthCheckResult = HealthCheckResult.getInstanceFromJsonMessage(name, response);
                } catch (IllegalArgumentException e) {
                    final String message = "IOException while parsing the health json message";
                    if (LOG.isErrorEnabled()) {
                        LOG.error(LogUtil.buildExceptionLogFormat(message), e.getClass().getName(), e.getMessage());
                    }
                    if (LOG.isDebugEnabled()) {
                        LOG.debug(message + ", json message: " + response, e);
                    }
                    healthCheckResult.setHealthy(false);
                }
            }
        } catch (IOException e) {
            final String message = "IOException while calling the URL " + healthUrlStr;
            if (LOG.isErrorEnabled()) {
                LOG.error(LogUtil.buildExceptionLogFormat(message), e.getClass().getName(), e.getMessage());
            }
            LOG.debug(message, e);

            healthCheckResult.setHealthy(false);
        }

        return healthCheckResult;
    }

    private static CAClientType parse(final InputStream configStream) throws ConfigurationException {
        synchronized (jaxbUnmarshallerLock) {
            Object root;
            try {
                if (jaxbUnmarshaller == null) {
                    JAXBContext context = JAXBContext.newInstance(ObjectFactory.class);
                    jaxbUnmarshaller = context.createUnmarshaller();

                    final SchemaFactory schemaFact = SchemaFactory
                            .newInstance(javax.xml.XMLConstants.W3C_XML_SCHEMA_NS_URI);
                    URL url = CAClientType.class.getResource("/xsd/caclient-conf.xsd");
                    jaxbUnmarshaller.setSchema(schemaFact.newSchema(url));
                }

                root = jaxbUnmarshaller.unmarshal(configStream);
            } catch (SAXException e) {
                throw new ConfigurationException("parse profile failed, message: " + e.getMessage(), e);
            } catch (JAXBException e) {
                throw new ConfigurationException(
                        "parse profile failed, message: " + XMLUtil.getMessage((JAXBException) e), e);
            }

            if (root instanceof JAXBElement) {
                return (CAClientType) ((JAXBElement<?>) root).getValue();
            } else {
                throw new ConfigurationException("invalid root element type");
            }
        }
    }

    private EnrollCertResult parseEnrollCertResult(final EnrollCertResultType result, final String caName)
            throws CAClientException {
        Map<String, CertOrError> certOrErrors = new HashMap<>();
        for (ResultEntryType resultEntry : result.getResultEntries()) {
            CertOrError certOrError;
            if (resultEntry instanceof EnrollCertResultEntryType) {
                EnrollCertResultEntryType entry = (EnrollCertResultEntryType) resultEntry;
                try {
                    java.security.cert.Certificate cert = getCertificate(entry.getCert());
                    certOrError = new CertOrError(cert);
                } catch (CertificateException e) {
                    throw new CAClientException("CertificateParsingException for request (id=" + entry.getId()
                            + "): " + e.getMessage());
                }
            } else if (resultEntry instanceof ErrorResultEntryType) {
                certOrError = new CertOrError(((ErrorResultEntryType) resultEntry).getStatusInfo());
            } else {
                certOrError = null;
            }

            certOrErrors.put(resultEntry.getId(), certOrError);
        }

        List<CMPCertificate> cmpCaPubs = result.getCACertificates();

        if (CollectionUtil.isEmpty(cmpCaPubs)) {
            return new EnrollCertResult(null, certOrErrors);
        }

        List<java.security.cert.Certificate> caPubs = new ArrayList<>(cmpCaPubs.size());
        for (CMPCertificate cmpCaPub : cmpCaPubs) {
            try {
                caPubs.add(getCertificate(cmpCaPub));
            } catch (CertificateException e) {
                final String message = "could not extract the caPub from CMPCertificate";
                if (LOG.isErrorEnabled()) {
                    LOG.error(LogUtil.buildExceptionLogFormat(message), e.getClass().getName(), e.getMessage());
                }
                LOG.debug(message, e);
            }
        }

        java.security.cert.Certificate caCert = null;
        for (CertOrError certOrError : certOrErrors.values()) {
            java.security.cert.Certificate cert = certOrError.getCertificate();
            if (cert == null) {
                continue;
            }

            for (java.security.cert.Certificate caPub : caPubs) {
                if (verify(caPub, cert)) {
                    caCert = caPub;
                    break;
                }
            }

            if (caCert != null) {
                break;
            }
        }

        if (caCert == null) {
            return new EnrollCertResult(null, certOrErrors);
        }

        for (CertOrError certOrError : certOrErrors.values()) {
            java.security.cert.Certificate cert = certOrError.getCertificate();
            if (cert == null) {
                continue;
            }

            if (verify(caCert, cert) == false) {
                LOG.warn("not all certificates are issued by CA embedded in caPubs, ignore the caPubs");
                return new EnrollCertResult(null, certOrErrors);
            }
        }

        return new EnrollCertResult(caCert, certOrErrors);
    }

}