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

Java tutorial

Introduction

Here is the source code for org.xipki.ca.client.impl.X509CmpRequestor.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.ByteArrayInputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.security.cert.CRLException;
import java.security.cert.CertificateException;
import java.security.cert.X509CRL;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
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 javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.ASN1Enumerated;
import org.bouncycastle.asn1.ASN1GeneralizedTime;
import org.bouncycastle.asn1.ASN1Integer;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.ASN1OctetString;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.DERUTF8String;
import org.bouncycastle.asn1.cmp.CMPCertificate;
import org.bouncycastle.asn1.cmp.CMPObjectIdentifiers;
import org.bouncycastle.asn1.cmp.CertRepMessage;
import org.bouncycastle.asn1.cmp.CertResponse;
import org.bouncycastle.asn1.cmp.CertifiedKeyPair;
import org.bouncycastle.asn1.cmp.ErrorMsgContent;
import org.bouncycastle.asn1.cmp.GenRepContent;
import org.bouncycastle.asn1.cmp.InfoTypeAndValue;
import org.bouncycastle.asn1.cmp.PKIBody;
import org.bouncycastle.asn1.cmp.PKIFailureInfo;
import org.bouncycastle.asn1.cmp.PKIFreeText;
import org.bouncycastle.asn1.cmp.PKIHeader;
import org.bouncycastle.asn1.cmp.PKIMessage;
import org.bouncycastle.asn1.cmp.PKIStatus;
import org.bouncycastle.asn1.cmp.PKIStatusInfo;
import org.bouncycastle.asn1.cmp.RevDetails;
import org.bouncycastle.asn1.cmp.RevRepContent;
import org.bouncycastle.asn1.cmp.RevReqContent;
import org.bouncycastle.asn1.crmf.AttributeTypeAndValue;
import org.bouncycastle.asn1.crmf.CertId;
import org.bouncycastle.asn1.crmf.CertReqMessages;
import org.bouncycastle.asn1.crmf.CertReqMsg;
import org.bouncycastle.asn1.crmf.CertRequest;
import org.bouncycastle.asn1.crmf.CertTemplateBuilder;
import org.bouncycastle.asn1.crmf.ProofOfPossession;
import org.bouncycastle.asn1.x509.CertificateList;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.Extensions;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.cmp.CMPException;
import org.bouncycastle.cert.cmp.CertificateConfirmationContent;
import org.bouncycastle.cert.cmp.CertificateConfirmationContentBuilder;
import org.bouncycastle.jce.provider.X509CRLObject;
import org.bouncycastle.operator.DigestCalculatorProvider;
import org.bouncycastle.operator.bc.BcDigestCalculatorProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xipki.ca.client.api.CertprofileInfo;
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.common.cmp.CmpUtil;
import org.xipki.ca.common.cmp.PKIResponse;
import org.xipki.common.CRLReason;
import org.xipki.common.CmpUtf8Pairs;
import org.xipki.common.ObjectIdentifiers;
import org.xipki.common.ParamChecker;
import org.xipki.common.RequestResponseDebug;
import org.xipki.common.XipkiCmpConstants;
import org.xipki.common.util.CollectionUtil;
import org.xipki.common.util.StringUtil;
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.xml.sax.SAXException;

/**
 * @author Lijun Liao
 */

abstract class X509CmpRequestor extends CmpRequestor {
    private final static DigestCalculatorProvider digesetCalculatorProvider = new BcDigestCalculatorProvider();
    private static final BigInteger MINUS_ONE = BigInteger.valueOf(-1);

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

    private final DocumentBuilder xmlDocBuilder;

    private boolean implicitConfirm = true;

    X509CmpRequestor(final X509Certificate requestorCert, final X509Certificate responderCert,
            final SecurityFactory securityFactory) {
        super(requestorCert, responderCert, securityFactory);
        xmlDocBuilder = newDocumentBuilder();
    }

    X509CmpRequestor(final ConcurrentContentSigner requestor, final X509Certificate responderCert,
            final SecurityFactory securityFactory, final boolean signRequest) {
        super(requestor, responderCert, securityFactory, signRequest);
        xmlDocBuilder = newDocumentBuilder();
    }

    private static DocumentBuilder newDocumentBuilder() {
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        dbf.setNamespaceAware(true);
        try {
            return dbf.newDocumentBuilder();
        } catch (ParserConfigurationException e) {
            throw new RuntimeException("could not create XML document builder", e);
        }
    }

    public CRLResultType generateCRL(final RequestResponseDebug debug)
            throws CmpRequestorException, PKIErrorException {
        int action = XipkiCmpConstants.ACTION_GEN_CRL;
        PKIMessage request = buildMessageWithXipkAction(action, null);
        PKIResponse response = signAndSend(request, debug);
        return evaluateCRLResponse(response, action);
    }

    public CRLResultType downloadCurrentCRL(final RequestResponseDebug debug)
            throws CmpRequestorException, PKIErrorException {
        return downloadCRL((BigInteger) null, debug);
    }

    public CRLResultType downloadCRL(final BigInteger crlNumber, final RequestResponseDebug debug)
            throws CmpRequestorException, PKIErrorException {
        Integer action = null;
        PKIMessage request;
        if (crlNumber == null) {
            ASN1ObjectIdentifier type = CMPObjectIdentifiers.it_currentCRL;
            request = buildMessageWithGeneralMsgContent(type, null);
        } else {
            action = XipkiCmpConstants.ACTION_GET_CRL_WITH_SN;
            request = buildMessageWithXipkAction(action, new ASN1Integer(crlNumber));
        }

        PKIResponse response = signAndSend(request, debug);
        return evaluateCRLResponse(response, action);
    }

    private CRLResultType evaluateCRLResponse(final PKIResponse response, final Integer xipkiAction)
            throws CmpRequestorException, PKIErrorException {
        checkProtection(response);

        PKIBody respBody = response.getPkiMessage().getBody();
        int bodyType = respBody.getType();

        if (PKIBody.TYPE_ERROR == bodyType) {
            ErrorMsgContent content = (ErrorMsgContent) respBody.getContent();
            throw new PKIErrorException(content.getPKIStatusInfo());
        } else if (PKIBody.TYPE_GEN_REP != bodyType) {
            throw new CmpRequestorException("unknown PKI body type " + bodyType + " instead the exceptected ["
                    + PKIBody.TYPE_GEN_REP + ", " + PKIBody.TYPE_ERROR + "]");
        }

        ASN1ObjectIdentifier expectedType = xipkiAction == null ? CMPObjectIdentifiers.it_currentCRL
                : ObjectIdentifiers.id_xipki_cmp;

        GenRepContent genRep = (GenRepContent) respBody.getContent();

        InfoTypeAndValue[] itvs = genRep.toInfoTypeAndValueArray();
        InfoTypeAndValue itv = null;
        if (itvs != null && itvs.length > 0) {
            for (InfoTypeAndValue m : itvs) {
                if (expectedType.equals(m.getInfoType())) {
                    itv = m;
                    break;
                }
            }
        }
        if (itv == null) {
            throw new CmpRequestorException("the response does not contain InfoTypeAndValue " + expectedType);
        }

        ASN1Encodable certListAsn1Object;
        if (xipkiAction == null) {
            certListAsn1Object = itv.getInfoValue();
        } else {
            certListAsn1Object = extractXipkiActionContent(itv.getInfoValue(), xipkiAction);
        }

        CertificateList certList = CertificateList.getInstance(certListAsn1Object);

        X509CRL crl;
        try {
            crl = new X509CRLObject(certList);
        } catch (CRLException e) {
            throw new CmpRequestorException("returned CRL is invalid: " + e.getMessage());
        }

        CRLResultType result = new CRLResultType();
        result.setCRL(crl);
        return result;
    }

    public RevokeCertResultType revokeCertificate(final RevokeCertRequestType request,
            final RequestResponseDebug debug) throws CmpRequestorException, PKIErrorException {
        PKIMessage reqMessage = buildRevokeCertRequest(request);
        PKIResponse response = signAndSend(reqMessage, debug);
        return parse(response, request.getRequestEntries());
    }

    public RevokeCertResultType unrevokeCertificate(final UnrevokeOrRemoveCertRequestType request,
            final RequestResponseDebug debug) throws CmpRequestorException, PKIErrorException {
        PKIMessage reqMessage = buildUnrevokeOrRemoveCertRequest(request, CRLReason.REMOVE_FROM_CRL.getCode());
        PKIResponse response = signAndSend(reqMessage, debug);
        return parse(response, request.getRequestEntries());
    }

    public RevokeCertResultType removeCertificate(final UnrevokeOrRemoveCertRequestType request,
            final RequestResponseDebug debug) throws CmpRequestorException, PKIErrorException {
        PKIMessage reqMessage = buildUnrevokeOrRemoveCertRequest(request, XipkiCmpConstants.CRL_REASON_REMOVE);
        PKIResponse response = signAndSend(reqMessage, debug);
        return parse(response, request.getRequestEntries());
    }

    private RevokeCertResultType parse(final PKIResponse response,
            final List<? extends IssuerSerialEntryType> reqEntries)
            throws CmpRequestorException, PKIErrorException {
        checkProtection(response);

        PKIBody respBody = response.getPkiMessage().getBody();
        int bodyType = respBody.getType();

        if (PKIBody.TYPE_ERROR == bodyType) {
            ErrorMsgContent content = (ErrorMsgContent) respBody.getContent();
            throw new PKIErrorException(content.getPKIStatusInfo());
        } else if (PKIBody.TYPE_REVOCATION_REP != bodyType) {
            throw new CmpRequestorException("unknown PKI body type " + bodyType + " instead the exceptected ["
                    + PKIBody.TYPE_REVOCATION_REP + ", " + PKIBody.TYPE_ERROR + "]");
        }

        RevRepContent content = (RevRepContent) respBody.getContent();
        PKIStatusInfo[] statuses = content.getStatus();
        if (statuses == null || statuses.length != reqEntries.size()) {
            throw new CmpRequestorException("incorrect number of status entries in response '" + statuses.length
                    + "' instead the exceptected '" + reqEntries.size() + "'");
        }

        CertId[] revCerts = content.getRevCerts();

        RevokeCertResultType result = new RevokeCertResultType();
        for (int i = 0; i < statuses.length; i++) {
            PKIStatusInfo statusInfo = statuses[i];
            int status = statusInfo.getStatus().intValue();
            IssuerSerialEntryType re = reqEntries.get(i);

            if (status != PKIStatus.GRANTED && status != PKIStatus.GRANTED_WITH_MODS) {
                PKIFreeText text = statusInfo.getStatusString();
                String statusString = text == null ? null : text.getStringAt(0).getString();

                ResultEntryType resultEntry = new ErrorResultEntryType(re.getId(), status,
                        statusInfo.getFailInfo().intValue(), statusString);
                result.addResultEntry(resultEntry);
                continue;
            }

            CertId certId = null;
            if (revCerts != null) {
                for (CertId _certId : revCerts) {
                    if (re.getIssuer().equals(_certId.getIssuer().getName())
                            && re.getSerialNumber().equals(_certId.getSerialNumber().getValue())) {
                        certId = _certId;
                        break;
                    }
                }
            }

            if (certId == null) {
                LOG.warn("certId is not present in response for (issuer='{}', serialNumber={})",
                        X509Util.getRFC4519Name(re.getIssuer()), re.getSerialNumber());
                certId = new CertId(new GeneralName(re.getIssuer()), re.getSerialNumber());
                continue;
            }

            ResultEntryType resultEntry = new RevokeCertResultEntryType(re.getId(), certId);
            result.addResultEntry(resultEntry);
        }

        return result;
    }

    public EnrollCertResultType requestCertificate(final P10EnrollCertRequestType p10Req, final String username,
            final RequestResponseDebug debug) throws CmpRequestorException, PKIErrorException {
        PKIMessage request = buildPKIMessage(p10Req, username);
        Map<BigInteger, String> reqIdIdMap = new HashMap<>();
        reqIdIdMap.put(MINUS_ONE, p10Req.getId());
        return intern_requestCertificate(request, reqIdIdMap, PKIBody.TYPE_CERT_REP, debug);
    }

    public EnrollCertResultType requestCertificate(final EnrollCertRequestType req, final String username,
            final RequestResponseDebug debug) throws CmpRequestorException, PKIErrorException {
        PKIMessage request = buildPKIMessage(req, username);
        Map<BigInteger, String> reqIdIdMap = new HashMap<>();
        List<EnrollCertRequestEntryType> reqEntries = req.getRequestEntries();

        for (EnrollCertRequestEntryType reqEntry : reqEntries) {
            reqIdIdMap.put(reqEntry.getCertReq().getCertReqId().getValue(), reqEntry.getId());
        }

        int exptectedBodyType;
        switch (req.getType()) {
        case CERT_REQ:
            exptectedBodyType = PKIBody.TYPE_CERT_REP;
            break;
        case KEY_UPDATE:
            exptectedBodyType = PKIBody.TYPE_KEY_UPDATE_REP;
            break;
        default:
            exptectedBodyType = PKIBody.TYPE_CROSS_CERT_REP;
        }

        return intern_requestCertificate(request, reqIdIdMap, exptectedBodyType, debug);
    }

    private EnrollCertResultType intern_requestCertificate(final PKIMessage reqMessage,
            final Map<BigInteger, String> reqIdIdMap, final int expectedBodyType, final RequestResponseDebug debug)
            throws CmpRequestorException, PKIErrorException {
        PKIResponse response = signAndSend(reqMessage, debug);
        checkProtection(response);

        PKIBody respBody = response.getPkiMessage().getBody();
        int bodyType = respBody.getType();

        if (PKIBody.TYPE_ERROR == bodyType) {
            ErrorMsgContent content = (ErrorMsgContent) respBody.getContent();
            throw new PKIErrorException(content.getPKIStatusInfo());
        }

        else if (expectedBodyType != bodyType) {
            throw new CmpRequestorException("unknown PKI body type " + bodyType + " instead the exceptected ["
                    + expectedBodyType + ", " + PKIBody.TYPE_ERROR + "]");
        }

        CertRepMessage certRep = (CertRepMessage) respBody.getContent();
        CertResponse[] certResponses = certRep.getResponse();

        EnrollCertResultType result = new EnrollCertResultType();

        // CA certificates
        CMPCertificate[] caPubs = certRep.getCaPubs();
        if (caPubs != null && caPubs.length > 0) {
            for (int i = 0; i < caPubs.length; i++) {
                if (caPubs[i] != null) {
                    result.addCACertificate(caPubs[i]);
                }
            }
        }

        boolean isImplicitConfirm = CmpUtil.isImplictConfirm(response.getPkiMessage().getHeader());

        CertificateConfirmationContentBuilder certConfirmBuilder = isImplicitConfirm ? null
                : new CertificateConfirmationContentBuilder();
        boolean requireConfirm = false;

        // We only accept the certificates which are requested.
        for (CertResponse certResp : certResponses) {
            PKIStatusInfo statusInfo = certResp.getStatus();
            int status = statusInfo.getStatus().intValue();
            BigInteger certReqId = certResp.getCertReqId().getValue();
            String thisId = reqIdIdMap.get(certReqId);
            if (thisId != null) {
                reqIdIdMap.remove(certReqId);
            } else if (reqIdIdMap.size() == 1) {
                thisId = reqIdIdMap.values().iterator().next();
                reqIdIdMap.clear();
            }

            if (thisId == null) {
                continue; // ignore it. this cert is not requested by me
            }

            ResultEntryType resultEntry;
            if (status == PKIStatus.GRANTED || status == PKIStatus.GRANTED_WITH_MODS) {
                CertifiedKeyPair cvk = certResp.getCertifiedKeyPair();
                if (cvk == null) {
                    return null;
                }

                CMPCertificate cmpCert = cvk.getCertOrEncCert().getCertificate();
                if (cmpCert == null) {
                    return null;
                }

                resultEntry = new EnrollCertResultEntryType(thisId, cmpCert, status);

                if (isImplicitConfirm == false) {
                    requireConfirm = true;
                    X509CertificateHolder certHolder = null;
                    try {
                        certHolder = new X509CertificateHolder(cmpCert.getEncoded());
                    } catch (IOException e) {
                        resultEntry = new ErrorResultEntryType(thisId, ClientErrorCode.PKIStatus_RESPONSE_ERROR,
                                PKIFailureInfo.systemFailure, "error while decode the certificate");
                    }

                    if (certHolder != null) {
                        certConfirmBuilder.addAcceptedCertificate(certHolder, certReqId);
                    }
                }
            } else {
                PKIFreeText statusString = statusInfo.getStatusString();
                String errorMessage = statusString == null ? null : statusString.getStringAt(0).getString();
                int failureInfo = statusInfo.getFailInfo().intValue();

                resultEntry = new ErrorResultEntryType(thisId, status, failureInfo, errorMessage);
            }
            result.addResultEntry(resultEntry);
        }

        if (CollectionUtil.isNotEmpty(reqIdIdMap)) {
            for (BigInteger reqId : reqIdIdMap.keySet()) {
                ErrorResultEntryType ere = new ErrorResultEntryType(reqIdIdMap.get(reqId),
                        ClientErrorCode.PKIStatus_NO_ANSWER);
                result.addResultEntry(ere);
            }
        }

        if (requireConfirm == false) {
            return result;
        }

        PKIMessage confirmRequest = buildCertConfirmRequest(response.getPkiMessage().getHeader().getTransactionID(),
                certConfirmBuilder);

        response = signAndSend(confirmRequest, debug);
        checkProtection(response);

        if (PKIBody.TYPE_ERROR == bodyType) {
            ErrorMsgContent content = (ErrorMsgContent) respBody.getContent();
            throw new PKIErrorException(content.getPKIStatusInfo());
        }

        return result;
    }

    private PKIMessage buildCertConfirmRequest(ASN1OctetString tid,
            CertificateConfirmationContentBuilder certConfirmBuilder) throws CmpRequestorException {
        PKIHeader header = buildPKIHeader(implicitConfirm, tid, null, (InfoTypeAndValue[]) null);
        CertificateConfirmationContent certConfirm;
        try {
            certConfirm = certConfirmBuilder.build(digesetCalculatorProvider);
        } catch (CMPException e) {
            throw new CmpRequestorException(e.getMessage(), e);
        }
        PKIBody body = new PKIBody(PKIBody.TYPE_CERT_CONFIRM, certConfirm.toASN1Structure());
        return new PKIMessage(header, body);
    }

    private PKIMessage buildRevokeCertRequest(final RevokeCertRequestType request) throws CmpRequestorException {
        PKIHeader header = buildPKIHeader(null);

        List<RevokeCertRequestEntryType> requestEntries = request.getRequestEntries();
        List<RevDetails> revDetailsArray = new ArrayList<>(requestEntries.size());
        for (RevokeCertRequestEntryType requestEntry : requestEntries) {
            CertTemplateBuilder certTempBuilder = new CertTemplateBuilder();
            certTempBuilder.setIssuer(requestEntry.getIssuer());
            certTempBuilder.setSerialNumber(new ASN1Integer(requestEntry.getSerialNumber()));

            Date invalidityDate = requestEntry.getInvalidityDate();
            Extension[] extensions = new Extension[invalidityDate == null ? 1 : 2];

            try {
                ASN1Enumerated reason = new ASN1Enumerated(requestEntry.getReason());
                extensions[0] = new Extension(Extension.reasonCode, true, new DEROctetString(reason.getEncoded()));

                if (invalidityDate != null) {
                    ASN1GeneralizedTime time = new ASN1GeneralizedTime(invalidityDate);
                    extensions[1] = new Extension(Extension.invalidityDate, true,
                            new DEROctetString(time.getEncoded()));
                }
            } catch (IOException e) {
                throw new CmpRequestorException(e.getMessage(), e);
            }
            Extensions exts = new Extensions(extensions);

            RevDetails revDetails = new RevDetails(certTempBuilder.build(), exts);
            revDetailsArray.add(revDetails);
        }

        RevReqContent content = new RevReqContent(revDetailsArray.toArray(new RevDetails[0]));
        PKIBody body = new PKIBody(PKIBody.TYPE_REVOCATION_REQ, content);
        return new PKIMessage(header, body);
    }

    private PKIMessage buildUnrevokeOrRemoveCertRequest(final UnrevokeOrRemoveCertRequestType request,
            final int reasonCode) throws CmpRequestorException {
        PKIHeader header = buildPKIHeader(null);

        List<IssuerSerialEntryType> requestEntries = request.getRequestEntries();
        List<RevDetails> revDetailsArray = new ArrayList<>(requestEntries.size());
        for (IssuerSerialEntryType requestEntry : requestEntries) {
            CertTemplateBuilder certTempBuilder = new CertTemplateBuilder();
            certTempBuilder.setIssuer(requestEntry.getIssuer());
            certTempBuilder.setSerialNumber(new ASN1Integer(requestEntry.getSerialNumber()));

            Extension[] extensions = new Extension[1];

            try {
                ASN1Enumerated reason = new ASN1Enumerated(reasonCode);
                extensions[0] = new Extension(Extension.reasonCode, true, new DEROctetString(reason.getEncoded()));
            } catch (IOException e) {
                throw new CmpRequestorException(e.getMessage(), e);
            }
            Extensions exts = new Extensions(extensions);

            RevDetails revDetails = new RevDetails(certTempBuilder.build(), exts);
            revDetailsArray.add(revDetails);
        }

        RevReqContent content = new RevReqContent(revDetailsArray.toArray(new RevDetails[0]));
        PKIBody body = new PKIBody(PKIBody.TYPE_REVOCATION_REQ, content);
        return new PKIMessage(header, body);
    }

    private PKIMessage buildPKIMessage(final P10EnrollCertRequestType p10Req, final String username) {
        CmpUtf8Pairs utf8Pairs = new CmpUtf8Pairs(CmpUtf8Pairs.KEY_CERT_PROFILE, p10Req.getCertprofile());
        if (StringUtil.isNotBlank(username)) {
            utf8Pairs.putUtf8Pair(CmpUtf8Pairs.KEY_USER, username);
        }

        PKIHeader header = buildPKIHeader(implicitConfirm, null, utf8Pairs);
        PKIBody body = new PKIBody(PKIBody.TYPE_P10_CERT_REQ, p10Req.getP10Req());

        return new PKIMessage(header, body);
    }

    private PKIMessage buildPKIMessage(final EnrollCertRequestType req, final String username) {
        PKIHeader header = buildPKIHeader(implicitConfirm, null, username);

        List<EnrollCertRequestEntryType> reqEntries = req.getRequestEntries();
        CertReqMsg[] certReqMsgs = new CertReqMsg[reqEntries.size()];

        for (int i = 0; i < reqEntries.size(); i++) {
            EnrollCertRequestEntryType reqEntry = reqEntries.get(i);
            CmpUtf8Pairs utf8Pairs = new CmpUtf8Pairs(CmpUtf8Pairs.KEY_CERT_PROFILE, reqEntry.getCertprofile());
            AttributeTypeAndValue certprofileInfo = CmpUtil.buildAttributeTypeAndValue(utf8Pairs);

            certReqMsgs[i] = new CertReqMsg(reqEntry.getCertReq(), reqEntry.getPopo(),
                    (certprofileInfo == null) ? null : new AttributeTypeAndValue[] { certprofileInfo });
        }

        int bodyType;
        switch (req.getType()) {
        case CERT_REQ:
            bodyType = PKIBody.TYPE_CERT_REQ;
            break;
        case KEY_UPDATE:
            bodyType = PKIBody.TYPE_KEY_UPDATE_REQ;
            break;
        default:
            bodyType = PKIBody.TYPE_CROSS_CERT_REQ;
        }

        PKIBody body = new PKIBody(bodyType, new CertReqMessages(certReqMsgs));

        PKIMessage pkiMessage = new PKIMessage(header, body);
        return pkiMessage;
    }

    private PKIMessage buildPKIMessage(final CertRequest req, final ProofOfPossession pop, final String profileName,
            final String username) {
        PKIHeader header = buildPKIHeader(implicitConfirm, null, username);

        CmpUtf8Pairs utf8Pairs = new CmpUtf8Pairs(CmpUtf8Pairs.KEY_CERT_PROFILE, profileName);
        AttributeTypeAndValue certprofileInfo = CmpUtil.buildAttributeTypeAndValue(utf8Pairs);
        CertReqMsg[] certReqMsgs = new CertReqMsg[1];
        certReqMsgs[0] = new CertReqMsg(req, pop, new AttributeTypeAndValue[] { certprofileInfo });
        PKIBody body = new PKIBody(PKIBody.TYPE_CERT_REQ, new CertReqMessages(certReqMsgs));

        return new PKIMessage(header, body);
    }

    public PKIMessage envelope(final CertRequest req, final ProofOfPossession pop, final String profileName,
            final String username) throws CmpRequestorException {
        PKIMessage request = buildPKIMessage(req, pop, profileName, username);
        return sign(request);
    }

    public PKIMessage envelopeRevocation(final RevokeCertRequestType request) throws CmpRequestorException {
        PKIMessage reqMessage = buildRevokeCertRequest(request);
        reqMessage = sign(reqMessage);
        return reqMessage;
    }

    public CAInfo retrieveCAInfo(final String caName, final RequestResponseDebug debug)
            throws CmpRequestorException, PKIErrorException {
        ASN1EncodableVector v = new ASN1EncodableVector();
        v.add(new ASN1Integer(2));
        ASN1Sequence acceptVersions = new DERSequence(v);

        int action = XipkiCmpConstants.ACTION_GET_CAINFO;
        PKIMessage request = buildMessageWithXipkAction(action, acceptVersions);
        PKIResponse response = signAndSend(request, debug);
        ASN1Encodable itvValue = extractXipkiActionRepContent(response, action);
        DERUTF8String utf8Str = DERUTF8String.getInstance(itvValue);
        String systemInfoStr = utf8Str.getString();

        LOG.debug("CAInfo for CA {}: {}", caName, systemInfoStr);
        Document doc;
        try {
            doc = xmlDocBuilder.parse(new ByteArrayInputStream(systemInfoStr.getBytes("UTF-8")));
        } catch (SAXException | IOException e) {
            throw new CmpRequestorException(
                    "could not parse the returned systemInfo for CA " + caName + ": " + e.getMessage(), e);
        }

        final String namespace = null;
        Element root = doc.getDocumentElement();
        String s = root.getAttribute("version");
        if (StringUtil.isBlank(s)) {
            s = root.getAttributeNS(namespace, "version");
        }

        int version = StringUtil.isBlank(s) ? 1 : Integer.parseInt(s);

        if (version == 2) {
            X509Certificate caCert;

            String b64CACert = XMLUtil.getValueOfFirstElementChild(root, namespace, "CACert");
            try {
                caCert = X509Util.parseBase64EncodedCert(b64CACert);
            } catch (CertificateException | IOException e) {
                throw new CmpRequestorException("could no parse the CA certificate", e);
            }

            Element profilesElement = XMLUtil.getFirstElementChild(root, namespace, "certprofiles");
            Set<CertprofileInfo> profiles = new HashSet<>();
            Set<String> profileNames = new HashSet<>();
            if (profilesElement != null) {
                List<Element> profileElements = XMLUtil.getElementChilden(profilesElement, namespace,
                        "certprofile");

                for (Element element : profileElements) {
                    String name = XMLUtil.getValueOfFirstElementChild(element, namespace, "name");
                    String type = XMLUtil.getValueOfFirstElementChild(element, namespace, "type");
                    String conf = XMLUtil.getValueOfFirstElementChild(element, namespace, "conf");
                    CertprofileInfo profile = new CertprofileInfo(name, type, conf);
                    profiles.add(profile);
                    profileNames.add(name);
                    if (LOG.isDebugEnabled()) {
                        StringBuilder sb = new StringBuilder();
                        sb.append("configured for CA ").append(caName).append(" certprofile (");
                        sb.append("name=").append(name).append(", ");
                        sb.append("type=").append(type).append(", ");
                        sb.append("conf=").append(conf).append(")");
                        LOG.debug(sb.toString());
                    }
                }
            }

            LOG.info("CA {} supports profiles {}", caName, profileNames);
            return new CAInfo(caCert, profiles);
        } else {
            throw new CmpRequestorException("unknown CAInfo version " + version);
        }
    }

    public RemoveExpiredCertsResult removeExpiredCerts(final String certprofile, final String userLike,
            final long overlapSeconds, final RequestResponseDebug debug)
            throws CmpRequestorException, PKIErrorException {
        ParamChecker.assertNotBlank("certprofile", certprofile);
        if (overlapSeconds < 0) {
            throw new IllegalArgumentException("overlapSeconds could not be negative");
        }

        StringBuilder sb = new StringBuilder();

        sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>");
        sb.append("<removeExpiredCertsReq version=\"1\">");
        // Profile
        sb.append("<certprofile>");
        sb.append(certprofile);
        sb.append("</certprofile>");

        // Username
        if (StringUtil.isNotBlank(userLike)) {
            sb.append("<userLike>");
            sb.append(userLike);
            sb.append("</userLike>");
        }

        sb.append("<overlap>");
        sb.append(overlapSeconds);
        sb.append("</overlap>");

        sb.append("</removeExpiredCertsReq>");

        String requestInfo = sb.toString();
        int action = XipkiCmpConstants.ACTION_REMOVE_EXPIRED_CERTS;
        PKIMessage request = buildMessageWithXipkAction(action, new DERUTF8String(requestInfo));
        PKIResponse response = signAndSend(request, debug);
        ASN1Encodable itvValue = extractXipkiActionRepContent(response, action);
        DERUTF8String utf8Str = DERUTF8String.getInstance(itvValue);
        String resultInfoStr = utf8Str.getString();

        if (LOG.isDebugEnabled()) {
            LOG.debug("removeExpiredCertsResp for (profile={}, usernameLike={}, overlapSeconds={}): {}",
                    new Object[] { certprofile, userLike, overlapSeconds, resultInfoStr });
        }
        Document doc;
        try {
            doc = xmlDocBuilder.parse(new ByteArrayInputStream(resultInfoStr.getBytes("UTF-8")));
        } catch (SAXException | IOException e) {
            throw new CmpRequestorException("could not parse the returned removeExpiredCertsResp", e);
        }

        String namespace = null;

        RemoveExpiredCertsResult result = new RemoveExpiredCertsResult();
        String nodeValue = XMLUtil.getValueOfFirstElementChild(doc.getDocumentElement(), namespace, "numCerts");
        if (nodeValue != null) {
            try {
                result.setNumOfCerts(Integer.parseInt(nodeValue));
            } catch (NumberFormatException e) {
                throw new CmpRequestorException("invalid numCerts '" + nodeValue + "'");
            }
        }

        nodeValue = XMLUtil.getValueOfFirstElementChild(doc.getDocumentElement(), namespace, "userLike");
        if (nodeValue != null) {
            result.setUserLike(nodeValue);
        }

        nodeValue = XMLUtil.getValueOfFirstElementChild(doc.getDocumentElement(), namespace, "profile");
        if (nodeValue != null) {
            result.setCertprofile(nodeValue);
        }

        nodeValue = XMLUtil.getValueOfFirstElementChild(doc.getDocumentElement(), namespace, "expiredAt");
        if (nodeValue != null) {
            try {
                result.setExpiredAt(Long.parseLong(nodeValue));
            } catch (NumberFormatException e) {
                throw new CmpRequestorException("invalid expiredAt '" + nodeValue + "'");
            }
        }

        return result;
    }

}