/*
*
* Copyright (c) 2013 - 2017 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.pki.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.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.X509CRL;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xipki.commons.common.HealthCheckResult;
import org.xipki.commons.common.ObjectCreationException;
import org.xipki.commons.common.RequestResponseDebug;
import org.xipki.commons.common.util.CollectionUtil;
import org.xipki.commons.common.util.CompareUtil;
import org.xipki.commons.common.util.IoUtil;
import org.xipki.commons.common.util.LogUtil;
import org.xipki.commons.common.util.ParamUtil;
import org.xipki.commons.common.util.XmlUtil;
import org.xipki.commons.security.AlgorithmValidator;
import org.xipki.commons.security.CollectionAlgorithmValidator;
import org.xipki.commons.security.ConcurrentContentSigner;
import org.xipki.commons.security.SecurityFactory;
import org.xipki.commons.security.SignerConf;
import org.xipki.commons.security.XiSecurityConstants;
import org.xipki.commons.security.util.X509Util;
import org.xipki.pki.ca.client.api.CaClient;
import org.xipki.pki.ca.client.api.CaClientException;
import org.xipki.pki.ca.client.api.CertIdOrError;
import org.xipki.pki.ca.client.api.CertOrError;
import org.xipki.pki.ca.client.api.CertprofileInfo;
import org.xipki.pki.ca.client.api.EnrollCertResult;
import org.xipki.pki.ca.client.api.PkiErrorException;
import org.xipki.pki.ca.client.api.dto.CsrEnrollCertRequest;
import org.xipki.pki.ca.client.api.dto.EnrollCertRequest;
import org.xipki.pki.ca.client.api.dto.EnrollCertRequestEntry;
import org.xipki.pki.ca.client.api.dto.EnrollCertResultEntry;
import org.xipki.pki.ca.client.api.dto.EnrollCertResultResp;
import org.xipki.pki.ca.client.api.dto.ErrorResultEntry;
import org.xipki.pki.ca.client.api.dto.ResultEntry;
import org.xipki.pki.ca.client.api.dto.RevokeCertRequest;
import org.xipki.pki.ca.client.api.dto.RevokeCertRequestEntry;
import org.xipki.pki.ca.client.api.dto.RevokeCertResultEntry;
import org.xipki.pki.ca.client.api.dto.RevokeCertResultType;
import org.xipki.pki.ca.client.api.dto.UnrevokeOrRemoveCertEntry;
import org.xipki.pki.ca.client.api.dto.UnrevokeOrRemoveCertRequest;
import org.xipki.pki.ca.client.impl.jaxb.CAClientType;
import org.xipki.pki.ca.client.impl.jaxb.CAType;
import org.xipki.pki.ca.client.impl.jaxb.CertprofileType;
import org.xipki.pki.ca.client.impl.jaxb.CertprofilesType;
import org.xipki.pki.ca.client.impl.jaxb.CmpControlType;
import org.xipki.pki.ca.client.impl.jaxb.FileOrValueType;
import org.xipki.pki.ca.client.impl.jaxb.ObjectFactory;
import org.xipki.pki.ca.client.impl.jaxb.RequestorType;
import org.xipki.pki.ca.client.impl.jaxb.ResponderType;
import org.xml.sax.SAXException;
/**
* @author Lijun Liao
* @since 2.0.0
*/
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;
}
StringBuilder sb = new StringBuilder("scheduled configuring CAs ");
sb.append(autoConfCaNames);
LOG.info(sb.toString());
Set<String> failedCaNames = autoConfCas(autoConfCaNames);
if (CollectionUtil.isNonEmpty(failedCaNames)) {
LOG.warn("could not configure following CAs {}", failedCaNames);
}
} finally {
lastUpdate = System.currentTimeMillis();
inProcess.set(false);
}
}
} // class ClientConfigUpdater
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 final Set<String> autoConfCaNames = new HashSet<>();
private SecurityFactory securityFactory;
private String confFile;
private Map<X509Certificate, Boolean> tryNssToVerifyMap = new ConcurrentHashMap<>();
private ScheduledThreadPoolExecutor scheduledThreadPoolExecutor;
private AtomicBoolean initialized = new AtomicBoolean(false);
public CaClientImpl() {
}
public void setSecurityFactory(final SecurityFactory securityFactory) {
this.securityFactory = securityFactory;
}
/**
*
* @return names of CAs which must not been configured.
*/
private Set<String> autoConfCas(Set<String> caNames) {
if (caNames.isEmpty()) {
return Collections.emptySet();
}
Set<String> caNamesWithError = new HashSet<>();
Set<String> errorCaNames = new HashSet<>();
for (String name : caNames) {
CaConf ca = casMap.get(name);
try {
CaInfo caInfo = ca.getRequestor().retrieveCaInfo(name, null);
if (ca.isCertAutoconf()) {
ca.setCert(caInfo.getCert());
}
if (ca.isCertprofilesAutoconf()) {
ca.setCertprofiles(caInfo.getCertprofiles());
}
if (ca.isCmpControlAutoconf()) {
ca.setCmpControl(caInfo.getCmpControl());
}
LOG.info("retrieved CAInfo for CA " + name);
} catch (CmpRequestorException | PkiErrorException | CertificateEncodingException
| RuntimeException ex) {
errorCaNames.add(name);
caNamesWithError.add(name);
LogUtil.error(LOG, ex, "could not retrieve CAInfo for CA " + name);
}
}
return caNamesWithError;
} // method autoConfCas
public void init() throws CaClientException {
doInit(true);
}
private synchronized void doInit(final boolean force) throws CaClientException {
if (confFile == null) {
throw new IllegalStateException("confFile is not set");
}
if (securityFactory == null) {
throw new IllegalStateException("securityFactory is not set");
}
if (!force && initialized.get()) {
return;
}
// reset
this.casMap.clear();
this.autoConfCaNames.clear();
if (this.scheduledThreadPoolExecutor != null) {
this.scheduledThreadPoolExecutor.shutdownNow();
}
this.initialized.set(false);
LOG.info("initializing ...");
File configFile = new File(IoUtil.expandFilepath(confFile));
if (!configFile.exists()) {
throw new CaClientException("could not find configuration file " + confFile);
}
CAClientType config;
try {
config = parse(new FileInputStream(configFile));
} catch (FileNotFoundException ex) {
throw new CaClientException("could not read file " + confFile);
}
int numActiveCAs = 0;
for (CAType caType : config.getCAs().getCA()) {
if (!caType.isEnabled()) {
LOG.info("CA " + caType.getName() + " is disabled");
continue;
}
numActiveCAs++;
}
if (numActiveCAs == 0) {
LOG.warn("no active CA is configured");
}
Boolean bo = config.isDevMode();
boolean devMode = bo != null && bo.booleanValue();
// responders
Map<String, CmpResponder> responders = new HashMap<>();
for (ResponderType m : config.getResponders().getResponder()) {
X509Certificate cert;
try {
cert = X509Util.parseCert(readData(m.getCert()));
} catch (CertificateException | IOException ex) {
LogUtil.error(LOG, ex, "could not configure responder " + m.getName());
throw new CaClientException(ex.getMessage(), ex);
}
Set<String> algoNames = new HashSet<>();
for (String algo : m.getSignatureAlgos().getSignatureAlgo()) {
algoNames.add(algo);
}
AlgorithmValidator sigAlgoValidator;
try {
sigAlgoValidator = new CollectionAlgorithmValidator(algoNames);
} catch (NoSuchAlgorithmException ex) {
throw new CaClientException(ex.getMessage());
}
responders.put(m.getName(), new CmpResponder(cert, sigAlgoValidator));
}
// CA
Set<CaConf> cas = new HashSet<>();
for (CAType caType : config.getCAs().getCA()) {
bo = caType.isEnabled();
if (!bo.booleanValue()) {
continue;
}
String caName = caType.getName();
try {
// responder
CmpResponder responder = responders.get(caType.getResponder());
if (responder == null) {
throw new CaClientException("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(false);
ca.setCert(X509Util.parseCert(readData(caType.getCaCert().getCert())));
}
// CMPControl
CmpControlType cmpCtrlType = caType.getCmpControl();
if (cmpCtrlType.getAutoconf() != null) {
ca.setCmpControlAutoconf(true);
} else {
ca.setCmpControlAutoconf(false);
Boolean tmpBo = cmpCtrlType.isRrAkiRequired();
ClientCmpControl control = new ClientCmpControl(
(tmpBo == null) ? false : tmpBo.booleanValue());
ca.setCmpControl(control);
}
// 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);
if (ca.isCertAutoconf() || ca.isCertprofilesAutoconf()
|| ca.isCmpControlAutoconf()) {
autoConfCaNames.add(caName);
}
} catch (IOException | CertificateException ex) {
LogUtil.error(LOG, ex, "could not configure CA " + caName);
if (!devMode) {
throw new CaClientException(ex.getMessage(), ex);
}
}
}
// 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 ex) {
throw new CaClientException(ex.getMessage(), ex);
}
}
if (requestorConf.getSignerType() != null) {
try {
SignerConf signerConf = new SignerConf(requestorConf.getSignerConf());
ConcurrentContentSigner requestorSigner = securityFactory.createSigner(
requestorConf.getSignerType(), signerConf, requestorCert);
requestorSigners.put(name, requestorSigner);
} catch (ObjectCreationException ex) {
throw new CaClientException(ex.getMessage(), ex);
}
} else {
if (requestorConf.isSignRequest()) {
throw new CaClientException("signer of requestor must be configured");
} else if (requestorCert == null) {
throw new CaClientException(
"at least one of certificate and signer of requestor must be configured");
}
}
}
for (CaConf ca :cas) {
if (this.casMap.containsKey(ca.getName())) {
throw new CaClientException("duplicate CAs with the same name " + ca.getName());
}
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 CaClientException("could not find requestor named " + requestorName
+ " for CA " + ca.getName());
}
ca.setRequestor(cmpRequestor);
this.casMap.put(ca.getName(), ca);
}
if (!autoConfCaNames.isEmpty()) {
Integer caInfoUpdateInterval = config.getCAs().getCAInfoUpdateInterval();
if (caInfoUpdateInterval == null) {
caInfoUpdateInterval = 10;
} else if (caInfoUpdateInterval <= 0) {
caInfoUpdateInterval = 0;
} else if (caInfoUpdateInterval < 5) {
caInfoUpdateInterval = 5;
}
LOG.info("configuring CAs {}", autoConfCaNames);
Set<String> failedCaNames = autoConfCas(autoConfCaNames);
// try to re-configure the failed CAs
if (CollectionUtil.isNonEmpty(failedCaNames)) {
for (int i = 0; i < 3; i++) {
LOG.info("configuring ({}-th retry) CAs {}", i + 1, failedCaNames);
failedCaNames = autoConfCas(failedCaNames);
if (CollectionUtil.isEmpty(failedCaNames)) {
break;
}
try {
Thread.sleep(10000);
} catch (InterruptedException ex) {
LOG.warn("interrupted", ex);
}
}
}
if (CollectionUtil.isNonEmpty(failedCaNames)) {
final String msg = "could not configure following CAs " + failedCaNames;
if (devMode) {
LOG.warn(msg);
} else {
throw new CaClientException(msg);
}
}
if (caInfoUpdateInterval > 0) {
scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(1);
scheduledThreadPoolExecutor.scheduleAtFixedRate(new ClientConfigUpdater(),
caInfoUpdateInterval, caInfoUpdateInterval, TimeUnit.MINUTES);
}
}
initialized.set(true);
LOG.info("initialized");
} // method init
public void shutdown() {
if (scheduledThreadPoolExecutor != null) {
scheduledThreadPoolExecutor.shutdown();
while (!scheduledThreadPoolExecutor.isTerminated()) {
try {
Thread.sleep(100);
} catch (InterruptedException ex) {
LOG.warn("interrupted: {}", ex.getMessage());
}
}
scheduledThreadPoolExecutor = null;
}
}
@Override
public EnrollCertResult requestCert(final String caName, final CertificationRequest csr,
final String profile, final Date notBefore, final Date notAfter,
final RequestResponseDebug debug) throws CaClientException, PkiErrorException {
ParamUtil.requireNonNull("csr", csr);
String tmpCaName = caName;
if (tmpCaName == null) {
tmpCaName = getCaNameForProfile(profile);
}
if (tmpCaName == null) {
throw new CaClientException("certprofile " + profile + " is not supported by any CA");
}
CaConf ca = casMap.get(tmpCaName.trim());
if (ca == null) {
throw new CaClientException("could not find CA named " + tmpCaName);
}
final String id = "cert-1";
CsrEnrollCertRequest request = new CsrEnrollCertRequest(id, profile, csr);
EnrollCertResultResp result;
try {
result = ca.getRequestor().requestCertificate(request, notBefore, notAfter, debug);
} catch (CmpRequestorException ex) {
throw new CaClientException(ex.getMessage(), ex);
}
return parseEnrollCertResult(result, tmpCaName);
} // method requestCert
@Override
public EnrollCertResult requestCerts(final String caName, final EnrollCertRequest request,
final RequestResponseDebug debug) throws CaClientException, PkiErrorException {
ParamUtil.requireNonNull("request", request);
List<EnrollCertRequestEntry> requestEntries = request.getRequestEntries();
if (CollectionUtil.isEmpty(requestEntries)) {
return null;
}
String tmpCaName = caName;
boolean bo = (tmpCaName != null);
if (tmpCaName == null) {
// detect the CA name
String profile = requestEntries.get(0).getCertprofile();
tmpCaName = getCaNameForProfile(profile);
if (tmpCaName == null) {
throw new CaClientException("certprofile " + profile
+ " is not supported by any CA");
}
}
if (bo || request.getRequestEntries().size() > 1) {
// make sure that all requests are targeted on the same CA
for (EnrollCertRequestEntry entry : request.getRequestEntries()) {
String profile = entry.getCertprofile();
checkCertprofileSupportInCa(profile, tmpCaName);
}
}
CaConf ca = casMap.get(tmpCaName.trim());
if (ca == null) {
throw new CaClientException("could not find CA named " + tmpCaName);
}
EnrollCertResultResp result;
try {
result = ca.getRequestor().requestCertificate(request, debug);
} catch (CmpRequestorException ex) {
throw new CaClientException(ex.getMessage(), ex);
}
return parseEnrollCertResult(result, tmpCaName);
} // method requestCerts
private void checkCertprofileSupportInCa(final String certprofile, final String caName)
throws CaClientException {
String tmpCaName = caName;
if (tmpCaName != null) {
CaConf ca = casMap.get(tmpCaName.trim());
if (ca == null) {
throw new CaClientException("unknown ca: " + tmpCaName);
}
if (!ca.supportsProfile(certprofile)) {
throw new CaClientException("certprofile " + certprofile
+ " is not supported by the CA " + tmpCaName);
}
return;
}
for (CaConf ca : casMap.values()) {
if (!ca.isCaInfoConfigured()) {
continue;
}
if (!ca.supportsProfile(certprofile)) {
continue;
}
if (tmpCaName == null) {
tmpCaName = ca.getName();
} else {
throw new CaClientException("certprofile " + certprofile
+ " supported by more than one CA, please specify the CA name.");
}
}
if (tmpCaName == null) {
throw new CaClientException("unsupported certprofile " + certprofile);
}
}
@Override
public CertIdOrError revokeCert(final String caName, final X509Certificate cert,
final int reason, final Date invalidityDate, final RequestResponseDebug debug)
throws CaClientException, PkiErrorException {
ParamUtil.requireNonNull("cert", cert);
CaConf ca = getCa(caName);
assertIssuedByCa(cert, ca);
return revokeCert(ca, cert.getSerialNumber(), reason, invalidityDate, debug);
}
@Override
public CertIdOrError revokeCert(final String caName, final BigInteger serial,
final int reason, final Date invalidityDate, final RequestResponseDebug debug)
throws CaClientException, PkiErrorException {
CaConf ca = getCa(caName);
return revokeCert(ca, serial, reason, invalidityDate, debug);
}
private CertIdOrError revokeCert(final CaConf ca, final BigInteger serial,
final int reason, final Date invalidityDate, final RequestResponseDebug debug)
throws CaClientException, PkiErrorException {
ParamUtil.requireNonNull("ca", ca);
ParamUtil.requireNonNull("serial", serial);
final String id = "cert-1";
RevokeCertRequestEntry entry = new RevokeCertRequestEntry(id, ca.getSubject(), serial,
reason, invalidityDate);
if (ca.getCmpControl().isRrAkiRequired()) {
entry.setAuthorityKeyIdentifier(ca.getSubjectKeyIdentifier());
}
RevokeCertRequest request = new RevokeCertRequest();
request.addRequestEntry(entry);
Map<String, CertIdOrError> result = revokeCerts(request, debug);
return (result == null) ? null : result.get(id);
}
@Override
public Map<String, CertIdOrError> revokeCerts(final RevokeCertRequest request,
final RequestResponseDebug debug) throws CaClientException, PkiErrorException {
ParamUtil.requireNonNull("request", request);
List<RevokeCertRequestEntry> 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())) {
throw new PkiErrorException(PKIStatus.REJECTION, PKIFailureInfo.badRequest,
"revoking certificates issued by more than one CA is not allowed");
}
}
final String caName = getCaNameByIssuer(issuer);
CaConf caConf = casMap.get(caName);
if (caConf.getCmpControl().isRrAkiRequired()) {
byte[] aki = caConf.getSubjectKeyIdentifier();
List<RevokeCertRequestEntry> entries = request.getRequestEntries();
for (RevokeCertRequestEntry entry : entries) {
if (entry.getAuthorityKeyIdentifier() == null) {
entry.setAuthorityKeyIdentifier(aki);
}
}
}
X509CmpRequestor cmpRequestor = caConf.getRequestor();
RevokeCertResultType result;
try {
result = cmpRequestor.revokeCertificate(request, debug);
} catch (CmpRequestorException ex) {
throw new CaClientException(ex.getMessage(), ex);
}
return parseRevokeCertResult(result);
}
private Map<String, CertIdOrError> parseRevokeCertResult(final RevokeCertResultType result)
throws CaClientException {
Map<String, CertIdOrError> ret = new HashMap<>();
for (ResultEntry re : result.getResultEntries()) {
CertIdOrError certIdOrError;
if (re instanceof RevokeCertResultEntry) {
RevokeCertResultEntry entry = (RevokeCertResultEntry) re;
certIdOrError = new CertIdOrError(entry.getCertId());
} else if (re instanceof ErrorResultEntry) {
ErrorResultEntry entry = (ErrorResultEntry) re;
certIdOrError = new CertIdOrError(entry.getStatusInfo());
} else {
throw new CaClientException("unknown type " + re.getClass().getName());
}
ret.put(re.getId(), certIdOrError);
}
return ret;
}
@Override
public X509CRL downloadCrl(final String caName, final RequestResponseDebug debug)
throws CaClientException, PkiErrorException {
ParamUtil.requireNonNull("caName", caName);
return downloadCrl(caName, (BigInteger) null, debug);
}
@Override
public X509CRL downloadCrl(final String caName, final BigInteger crlNumber,
final RequestResponseDebug debug) throws CaClientException, PkiErrorException {
ParamUtil.requireNonNull("caName", caName);
doInit(false);
CaConf ca = casMap.get(caName.trim());
if (ca == null) {
throw new IllegalArgumentException("unknown CA " + caName);
}
X509CmpRequestor requestor = ca.getRequestor();
X509CRL result;
try {
result = (crlNumber == null) ? requestor.downloadCurrentCrl(debug)
: requestor.downloadCrl(crlNumber, debug);
} catch (CmpRequestorException ex) {
throw new CaClientException(ex.getMessage(), ex);
}
return result;
}
@Override
public X509CRL generateCrl(final String caName, final RequestResponseDebug debug)
throws CaClientException, PkiErrorException {
ParamUtil.requireNonNull("caName", caName);
CaConf ca = casMap.get(caName.trim());
if (ca == null) {
throw new IllegalArgumentException("unknown CA " + caName);
}
X509CmpRequestor requestor = ca.getRequestor();
try {
return requestor.generateCrl(debug);
} catch (CmpRequestorException ex) {
throw new CaClientException(ex.getMessage(), ex);
}
}
@Override
public String getCaNameByIssuer(final X500Name issuer) throws CaClientException {
ParamUtil.requireNonNull("issuer", issuer);
for (String name : casMap.keySet()) {
final CaConf ca = casMap.get(name);
if (!ca.isCaInfoConfigured()) {
continue;
}
if (CompareUtil.equalsObject(ca.getSubject(), 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()) {
continue;
}
if (!ca.supportsProfile(certprofile)) {
continue;
}
if (caName == null) {
caName = ca.getName();
} else {
throw new CaClientException("certprofile " + 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 : X509Util.toX509Cert(bcCert);
}
public String getConfFile() {
return confFile;
}
public void setConfFile(String confFile) {
this.confFile = ParamUtil.requireNonBlank("confFile", confFile);
}
@Override
public Set<String> getCaNames() {
return casMap.keySet();
}
@Override
public byte[] envelope(final CertRequest certRequest, final ProofOfPossession pop,
final String profileName, final String caName)
throws CaClientException {
ParamUtil.requireNonNull("certRequest", certRequest);
ParamUtil.requireNonNull("pop", pop);
ParamUtil.requireNonNull("profileName", profileName);
doInit(false);
String tmpCaName = caName;
if (tmpCaName == null) {
// detect the CA name
tmpCaName = getCaNameForProfile(profileName);
if (tmpCaName == null) {
throw new CaClientException("certprofile " + profileName
+ " is not supported by any CA");
}
} else {
checkCertprofileSupportInCa(profileName, tmpCaName);
}
CaConf ca = casMap.get(tmpCaName.trim());
if (ca == null) {
throw new CaClientException("could not find CA named " + tmpCaName);
}
PKIMessage pkiMessage;
try {
pkiMessage = ca.getRequestor().envelope(certRequest, pop, profileName);
} catch (CmpRequestorException ex) {
throw new CaClientException("CmpRequestorException: " + ex.getMessage(), ex);
}
try {
return pkiMessage.getEncoded();
} catch (IOException ex) {
throw new CaClientException("IOException: " + ex.getMessage(), ex);
}
} // method envelope
private boolean verify(final java.security.cert.Certificate caCert,
final java.security.cert.Certificate cert) {
if (!(caCert instanceof X509Certificate)) {
return false;
}
if (!(cert instanceof X509Certificate)) {
return false;
}
X509Certificate x509caCert = (X509Certificate) caCert;
X509Certificate x509cert = (X509Certificate) cert;
if (!x509cert.getIssuerX500Principal().equals(x509caCert.getSubjectX500Principal())) {
return false;
}
boolean inLoadTest = Boolean.getBoolean("org.xipki.loadtest");
if (inLoadTest) {
return true;
}
final String provider = XiSecurityConstants.PROVIDER_NAME_NSS;
Boolean tryNssToVerify = tryNssToVerifyMap.get(x509caCert);
PublicKey caPublicKey = x509caCert.getPublicKey();
try {
if (tryNssToVerify == null) {
if (Security.getProvider(provider) == null) {
tryNssToVerify = Boolean.FALSE;
tryNssToVerifyMap.put(x509caCert, tryNssToVerify);
} else {
byte[] tbs = x509cert.getTBSCertificate();
byte[] signatureValue = x509cert.getSignature();
String sigAlgName = x509cert.getSigAlgName();
try {
Signature verifier = Signature.getInstance(sigAlgName, provider);
verifier.initVerify(caPublicKey);
verifier.update(tbs);
LOG.info("use {} to verify {} signature", provider, sigAlgName);
tryNssToVerify = Boolean.TRUE;
tryNssToVerifyMap.put(x509caCert, tryNssToVerify);
return verifier.verify(signatureValue);
} catch (Exception ex) {
LOG.info("could not use {} to verify {} signature", provider, sigAlgName);
tryNssToVerify = Boolean.FALSE;
tryNssToVerifyMap.put(x509caCert, tryNssToVerify);
}
}
}
if (tryNssToVerify) {
byte[] tbs = x509cert.getTBSCertificate();
byte[] signatureValue = x509cert.getSignature();
String sigAlgName = x509cert.getSigAlgName();
Signature verifier = Signature.getInstance(sigAlgName, provider);
verifier.initVerify(caPublicKey);
verifier.update(tbs);
return verifier.verify(signatureValue);
} else {
x509cert.verify(caPublicKey);
return true;
}
} catch (SignatureException | InvalidKeyException | CertificateException
| NoSuchAlgorithmException | NoSuchProviderException ex) {
LOG.debug("{} while verifying signature: {}", ex.getClass().getName(), ex.getMessage());
return false;
}
} // method verify
@Override
public byte[] envelopeRevocation(final X500Name issuer, final BigInteger serial,
final int reason) throws CaClientException {
ParamUtil.requireNonNull("issuer", issuer);
doInit(false);
final String id = "cert-1";
RevokeCertRequestEntry entry = new RevokeCertRequestEntry(id, issuer, serial, reason, null);
RevokeCertRequest request = new RevokeCertRequest();
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 ex) {
throw new CaClientException(ex.getMessage(), ex);
}
}
@Override
public byte[] envelopeRevocation(final X509Certificate cert, final int reason)
throws CaClientException {
ParamUtil.requireNonNull("cert", cert);
X500Name issuer = X500Name.getInstance(cert.getIssuerX500Principal().getEncoded());
return envelopeRevocation(issuer, cert.getSerialNumber(), reason);
}
@Override
public CertIdOrError unrevokeCert(final String caName, final X509Certificate cert,
final RequestResponseDebug debug) throws CaClientException, PkiErrorException {
ParamUtil.requireNonNull("cert", cert);
CaConf ca = getCa(caName);
assertIssuedByCa(cert, ca);
return unrevokeCert(ca, cert.getSerialNumber(), debug);
}
@Override
public CertIdOrError unrevokeCert(final String caName, final BigInteger serial,
final RequestResponseDebug debug) throws CaClientException, PkiErrorException {
CaConf ca = getCa(caName);
return unrevokeCert(ca, serial, debug);
}
private CertIdOrError unrevokeCert(final CaConf ca, final BigInteger serial,
final RequestResponseDebug debug) throws CaClientException, PkiErrorException {
ParamUtil.requireNonNull("ca", ca);
ParamUtil.requireNonNull("serial", serial);
final String id = "cert-1";
UnrevokeOrRemoveCertEntry entry = new UnrevokeOrRemoveCertEntry(id, ca.getSubject(),
serial);
if (ca.getCmpControl().isRrAkiRequired()) {
entry.setAuthorityKeyIdentifier(ca.getSubjectKeyIdentifier());
}
UnrevokeOrRemoveCertRequest request = new UnrevokeOrRemoveCertRequest();
request.addRequestEntry(entry);
Map<String, CertIdOrError> result = unrevokeCerts(request, debug);
return (result == null) ? null : result.get(id);
}
@Override
public Map<String, CertIdOrError> unrevokeCerts(final UnrevokeOrRemoveCertRequest request,
final RequestResponseDebug debug) throws CaClientException, PkiErrorException {
ParamUtil.requireNonNull("request", request);
doInit(false);
List<UnrevokeOrRemoveCertEntry> 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())) {
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 ex) {
throw new CaClientException(ex.getMessage(), ex);
}
return parseRevokeCertResult(result);
} // method unrevokeCerts
@Override
public CertIdOrError removeCert(final String caName, final X509Certificate cert,
final RequestResponseDebug debug)
throws CaClientException, PkiErrorException {
ParamUtil.requireNonNull("cert", cert);
CaConf ca = getCa(caName);
assertIssuedByCa(cert, ca);
return removeCert(ca, cert.getSerialNumber(), debug);
}
@Override
public CertIdOrError removeCert(final String caName, final BigInteger serial,
final RequestResponseDebug debug)
throws CaClientException, PkiErrorException {
CaConf ca = getCa(caName);
return removeCert(ca, serial, debug);
}
private CertIdOrError removeCert(final CaConf ca, final BigInteger serial,
final RequestResponseDebug debug)
throws CaClientException, PkiErrorException {
ParamUtil.requireNonNull("ca", ca);
ParamUtil.requireNonNull("serial", serial);
final String id = "cert-1";
UnrevokeOrRemoveCertEntry entry = new UnrevokeOrRemoveCertEntry(id, ca.getSubject(),
serial);
if (ca.getCmpControl().isRrAkiRequired()) {
entry.setAuthorityKeyIdentifier(ca.getSubjectKeyIdentifier());
}
UnrevokeOrRemoveCertRequest request = new UnrevokeOrRemoveCertRequest();
request.addRequestEntry(entry);
Map<String, CertIdOrError> result = removeCerts(request, debug);
return (result == null) ? null : result.get(id);
}
@Override
public Map<String, CertIdOrError> removeCerts(final UnrevokeOrRemoveCertRequest request,
final RequestResponseDebug debug)
throws CaClientException, PkiErrorException {
ParamUtil.requireNonNull("request", request);
doInit(false);
List<UnrevokeOrRemoveCertEntry> 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())) {
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 ex) {
throw new CaClientException(ex.getMessage(), ex);
}
return parseRevokeCertResult(result);
}
@Override
public Set<CertprofileInfo> getCertprofiles(final String caName) throws CaClientException {
ParamUtil.requireNonNull("caName", caName);
doInit(false);
CaConf ca = casMap.get(caName.trim());
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 HealthCheckResult getHealthCheckResult(final String caName) throws CaClientException {
ParamUtil.requireNonNull("caName", caName);
String name = "X509CA";
HealthCheckResult healthCheckResult = new HealthCheckResult(name);
try {
doInit(false);
} catch (CaClientException ex) {
LogUtil.error(LOG, ex, "could not initialize CaCleint");
healthCheckResult.setHealthy(false);
return healthCheckResult;
}
CaConf ca = casMap.get(caName.trim());
if (ca == null) {
throw new IllegalArgumentException("unknown CA " + caName);
}
String healthUrlStr = ca.getHealthUrl();
URL serverUrl;
try {
serverUrl = new URL(healthUrlStr);
} catch (MalformedURLException ex) {
throw new CaClientException("invalid URL '" + healthUrlStr + "'");
}
try {
HttpURLConnection httpUrlConnection = IoUtil.openHttpConn(serverUrl);
InputStream inputStream = httpUrlConnection.getInputStream();
int responseCode = httpUrlConnection.getResponseCode();
if (responseCode != HttpURLConnection.HTTP_OK
&& responseCode != HttpURLConnection.HTTP_INTERNAL_ERROR) {
inputStream.close();
throw new IOException(String.format("bad response: code='%s', message='%s'",
httpUrlConnection.getResponseCode(),
httpUrlConnection.getResponseMessage()));
}
String responseContentType = httpUrlConnection.getContentType();
boolean isValidContentType = false;
if (responseContentType != null) {
if ("application/json".equalsIgnoreCase(responseContentType)) {
isValidContentType = true;
}
}
if (!isValidContentType) {
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 ex) {
LogUtil.error(LOG, ex, "IOException while parsing the health json message");
if (LOG.isDebugEnabled()) {
LOG.debug("json message: {}", response);
}
healthCheckResult.setHealthy(false);
}
}
} catch (IOException ex) {
LogUtil.error(LOG, ex, "IOException while fetching the URL " + healthUrlStr);
healthCheckResult.setHealthy(false);
}
return healthCheckResult;
} // method getHealthCheckResult
private EnrollCertResult parseEnrollCertResult(final EnrollCertResultResp result,
final String caName) throws CaClientException {
Map<String, CertOrError> certOrErrors = new HashMap<>();
for (ResultEntry resultEntry : result.getResultEntries()) {
CertOrError certOrError;
if (resultEntry instanceof EnrollCertResultEntry) {
EnrollCertResultEntry entry = (EnrollCertResultEntry) resultEntry;
try {
java.security.cert.Certificate cert = getCertificate(entry.getCert());
certOrError = new CertOrError(cert);
} catch (CertificateException ex) {
throw new CaClientException(String.format(
"CertificateParsingException for request (id=%s): %s",
entry.getId(), ex.getMessage()));
}
} else if (resultEntry instanceof ErrorResultEntry) {
certOrError = new CertOrError(((ErrorResultEntry) 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 ex) {
LogUtil.error(LOG, ex, "could not extract the caPub from CMPCertificate");
}
}
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)) {
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);
} // method parseEnrollCertResult
private static CAClientType parse(final InputStream configStream) throws CaClientException {
Object root;
synchronized (jaxbUnmarshallerLock) {
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 ex) {
throw new CaClientException("parsing profile failed, message: " + ex.getMessage(),
ex);
} catch (JAXBException ex) {
throw new CaClientException("parsing profile failed, message: "
+ XmlUtil.getMessage(ex), ex);
}
}
try {
configStream.close();
} catch (IOException ex) {
LOG.warn("could not close xmlConfStream: {}", ex.getMessage());
}
if (root instanceof JAXBElement) {
return (CAClientType) ((JAXBElement<?>) root).getValue();
} else {
throw new CaClientException("invalid root element type");
}
} // method parse
private static byte[] readData(final FileOrValueType fileOrValue) throws IOException {
byte[] data = fileOrValue.getValue();
if (data == null) {
data = IoUtil.read(fileOrValue.getFile());
}
return data;
}
private CaConf getCa(String caName) throws CaClientException {
String tmpCaName = caName;
if (tmpCaName == null) {
Iterator<String> names = casMap.keySet().iterator();
if (!names.hasNext()) {
throw new CaClientException("no CA is configured");
}
tmpCaName = names.next();
}
CaConf ca = casMap.get(tmpCaName.trim());
if (ca == null) {
throw new CaClientException("could not find CA named " + tmpCaName);
}
return ca;
}
private void assertIssuedByCa(X509Certificate cert, CaConf ca) throws CaClientException {
boolean issued;
try {
issued = X509Util.issues(ca.getCert(), cert);
} catch (CertificateEncodingException ex) {
LogUtil.error(LOG, ex);
issued = false;
}
if (!issued) {
throw new CaClientException("the given certificate is not issued by the CA");
}
}
}