/*
*
* 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.ocsp.server.impl.store.db;
import java.io.IOException;
import java.math.BigInteger;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.bouncycastle.util.encoders.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xipki.commons.common.util.CollectionUtil;
import org.xipki.commons.common.util.LogUtil;
import org.xipki.commons.common.util.ParamUtil;
import org.xipki.commons.common.util.StringUtil;
import org.xipki.commons.datasource.DataSourceWrapper;
import org.xipki.commons.datasource.springframework.dao.DataAccessException;
import org.xipki.commons.security.CertRevocationInfo;
import org.xipki.commons.security.HashAlgoType;
import org.xipki.commons.security.util.X509Util;
import org.xipki.pki.ocsp.api.CertStatusInfo;
import org.xipki.pki.ocsp.api.CertprofileOption;
import org.xipki.pki.ocsp.api.IssuerHashNameAndKey;
import org.xipki.pki.ocsp.api.OcspStore;
import org.xipki.pki.ocsp.api.OcspStoreException;
/**
* @author Lijun Liao
* @since 2.0.0
*/
public class DbCertStatusStore extends OcspStore {
private static class SimpleIssuerEntry {
private final int id;
private final Long revocationTimeMs;
SimpleIssuerEntry(final int id, final Long revocationTimeMs) {
this.id = id;
this.revocationTimeMs = revocationTimeMs;
}
public boolean match(final IssuerEntry issuer) {
if (id != issuer.getId()) {
return false;
}
if (revocationTimeMs == null) {
return issuer.getRevocationInfo() == null;
}
return (issuer.getRevocationInfo() == null) ? false
: revocationTimeMs == issuer.getRevocationInfo().getRevocationTime().getTime();
}
} // class SimpleIssuerEntry
private class StoreUpdateService implements Runnable {
@Override
public void run() {
initIssuerStore();
}
} // class StoreUpdateService
protected DataSourceWrapper datasource;
private static final Logger LOG = LoggerFactory.getLogger(DbCertStatusStore.class);
private final AtomicBoolean storeUpdateInProcess = new AtomicBoolean(false);
private String sqlCs;
private Map<HashAlgoType, String> sqlCsMap;
private IssuerFilter issuerFilter;
private IssuerStore issuerStore;
private boolean initialized;
private boolean initializationFailed;
private ScheduledThreadPoolExecutor scheduledThreadPoolExecutor;
protected List<Runnable> getScheduledServices() {
return Collections.emptyList();
}
private synchronized void initIssuerStore() {
if (storeUpdateInProcess.get()) {
return;
}
storeUpdateInProcess.set(true);
try {
if (isInitialized()) {
final String sql = "SELECT ID,REV,RT,S1C FROM ISSUER";
PreparedStatement ps = borrowPreparedStatement(sql);
ResultSet rs = null;
try {
Map<Integer, SimpleIssuerEntry> newIssuers = new HashMap<>();
rs = ps.executeQuery();
while (rs.next()) {
String sha1Fp = rs.getString("S1C");
if (!issuerFilter.includeIssuerWithSha1Fp(sha1Fp)) {
continue;
}
int id = rs.getInt("ID");
boolean revoked = rs.getBoolean("REV");
Long revTimeMs = revoked ? rs.getLong("RT") * 1000 : null;
SimpleIssuerEntry issuerEntry = new SimpleIssuerEntry(id, revTimeMs);
newIssuers.put(id, issuerEntry);
}
// no change in the issuerStore
Set<Integer> newIds = newIssuers.keySet();
Set<Integer> ids = (issuerStore != null) ? issuerStore.getIds()
: Collections.emptySet();
boolean issuersUnchanged = (ids.size() == newIds.size())
&& ids.containsAll(newIds) && newIds.containsAll(ids);
if (issuersUnchanged) {
for (Integer id : newIds) {
IssuerEntry entry = issuerStore.getIssuerForId(id);
SimpleIssuerEntry newEntry = newIssuers.get(id);
if (newEntry.match(entry)) {
issuersUnchanged = false;
break;
}
}
}
if (issuersUnchanged) {
return;
}
} finally {
releaseDbResources(ps, rs);
}
} // end if(initialized)
final String sql = "SELECT ID,NBEFORE,REV,RT,S1C,CERT,CRL_INFO FROM ISSUER";
PreparedStatement ps = borrowPreparedStatement(sql);
ResultSet rs = null;
try {
rs = ps.executeQuery();
List<IssuerEntry> caInfos = new LinkedList<>();
while (rs.next()) {
String sha1Fp = rs.getString("S1C");
if (!issuerFilter.includeIssuerWithSha1Fp(sha1Fp)) {
continue;
}
int id = rs.getInt("ID");
String b64Cert = rs.getString("CERT");
X509Certificate cert = X509Util.parseBase64EncodedCert(b64Cert);
IssuerEntry caInfoEntry = new IssuerEntry(id, cert);
String crlInfoStr = rs.getString("CRL_INFO");
if (StringUtil.isNotBlank(crlInfoStr)) {
CrlInfo crlInfo = new CrlInfo(crlInfoStr);
caInfoEntry.setCrlInfo(crlInfo);
}
IssuerHashNameAndKey sha1IssuerHash
= caInfoEntry.getIssuerHashNameAndKey(HashAlgoType.SHA1);
for (IssuerEntry existingIssuer : caInfos) {
if (existingIssuer.matchHash(HashAlgoType.SHA1,
sha1IssuerHash.getIssuerNameHash(),
sha1IssuerHash.getIssuerKeyHash())) {
throw new Exception(
"found at least two issuers with the same subject and key");
}
}
boolean revoked = rs.getBoolean("REV");
if (revoked) {
long lo = rs.getLong("RT");
caInfoEntry.setRevocationInfo(new Date(lo * 1000));
}
caInfos.add(caInfoEntry);
} // end while (rs.next())
initialized = false;
this.issuerStore = new IssuerStore(caInfos);
LOG.info("Updated issuers: {}", name);
initializationFailed = false;
initialized = true;
} finally {
releaseDbResources(ps, rs);
}
} catch (Throwable th) {
storeUpdateInProcess.set(false);
LogUtil.error(LOG, th, "could not executing initIssuerStore()");
initializationFailed = true;
initialized = true;
}
} // method initIssuerStore
@Override
public CertStatusInfo getCertStatus(final Date time, final HashAlgoType hashAlgo,
final byte[] issuerNameHash, final byte[] issuerKeyHash, final BigInteger serialNumber,
final boolean includeCertHash, final HashAlgoType certHashAlg,
final CertprofileOption certprofileOption) throws OcspStoreException {
ParamUtil.requireNonNull("hashAlgo", hashAlgo);
ParamUtil.requireNonNull("serialNumber", serialNumber);
if (serialNumber.signum() != 1) { // non-positive serial number
return CertStatusInfo.getUnknownCertStatusInfo(new Date(), null);
}
// wait for max. 0.5 second
int num = 5;
while (!isInitialized() && (num-- > 0)) {
try {
Thread.sleep(100);
} catch (InterruptedException ex) { // CHECKSTYLE:SKIP
}
}
if (!isInitialized()) {
throw new OcspStoreException("initialization of CertStore is still in process");
}
if (isInitializationFailed()) {
throw new OcspStoreException("initialization of CertStore failed");
}
String sql;
HashAlgoType certHashAlgo = null;
if (includeCertHash) {
certHashAlgo = (certHashAlg == null) ? hashAlgo : certHashAlg;
sql = sqlCsMap.get(certHashAlgo);
} else {
sql = sqlCs;
}
try {
IssuerEntry issuer = issuerStore.getIssuerForFp(hashAlgo, issuerNameHash,
issuerKeyHash);
if (issuer == null) {
return CertStatusInfo.getIssuerUnknownCertStatusInfo(new Date(), null);
}
CrlInfo crlInfo = issuer.getCrlInfo();
Date thisUpdate;
Date nextUpdate = null;
if (crlInfo != null && crlInfo.isUseCrlUpdates()) {
thisUpdate = crlInfo.getThisUpdate();
// this.nextUpdate is still in the future (10 seconds buffer)
if (crlInfo.getNextUpdate().getTime() > System.currentTimeMillis() + 10 * 1000) {
nextUpdate = crlInfo.getNextUpdate();
}
} else {
thisUpdate = new Date();
}
ResultSet rs = null;
CertStatusInfo certStatusInfo = null;
boolean unknown = true;
boolean ignore = false;
String certprofile = null;
String b64CertHash = null;
boolean revoked = false;
int reason = 0;
long revocationTime = 0;
long invalidatityTime = 0;
PreparedStatement ps = borrowPreparedStatement(sql);
try {
int idx = 1;
ps.setInt(idx++, issuer.getId());
ps.setString(idx++, serialNumber.toString(16));
rs = ps.executeQuery();
if (rs.next()) {
unknown = false;
long timeInSec = time.getTime() / 1000;
long notBeforeInSec = rs.getLong("NBEFORE");
if (!ignore && ignoreNotYetValidCert) {
if (notBeforeInSec != 0 && timeInSec < notBeforeInSec) {
ignore = true;
}
}
long notAfterInSec = rs.getLong("NAFTER");
if (!ignore && ignoreExpiredCert) {
if (notAfterInSec != 0 && timeInSec > notAfterInSec) {
ignore = true;
}
}
certprofile = rs.getString("PN");
if (!ignore) {
ignore = (certprofile != null) && (certprofileOption != null)
&& !certprofileOption.include(certprofile);
}
if (!ignore) {
if (certHashAlgo != null) {
b64CertHash = rs.getString(certHashAlgo.getShortName());
}
revoked = rs.getBoolean("REV");
if (revoked) {
reason = rs.getInt("RR");
revocationTime = rs.getLong("RT");
invalidatityTime = rs.getLong("RIT");
}
}
} // end if (rs.next())
} catch (SQLException ex) {
throw datasource.translate(sql, ex);
} finally {
releaseDbResources(ps, rs);
}
if (unknown) {
if (unknownSerialAsGood) {
certStatusInfo = CertStatusInfo.getGoodCertStatusInfo(certHashAlgo, null,
thisUpdate, nextUpdate, null);
} else {
certStatusInfo = CertStatusInfo.getUnknownCertStatusInfo(thisUpdate,
nextUpdate);
}
} else {
if (ignore) {
certStatusInfo = CertStatusInfo.getIgnoreCertStatusInfo(thisUpdate, nextUpdate);
} else {
byte[] certHash = null;
if (b64CertHash != null) {
certHash = Base64.decode(b64CertHash);
}
if (revoked) {
Date invTime = null;
if (invalidatityTime != 0 && invalidatityTime != revocationTime) {
invTime = new Date(invalidatityTime * 1000);
}
CertRevocationInfo revInfo = new CertRevocationInfo(reason,
new Date(revocationTime * 1000), invTime);
certStatusInfo = CertStatusInfo.getRevokedCertStatusInfo(revInfo,
certHashAlgo, certHash, thisUpdate, nextUpdate, certprofile);
} else {
certStatusInfo = CertStatusInfo.getGoodCertStatusInfo(certHashAlgo,
certHash, thisUpdate, nextUpdate, certprofile);
}
}
}
if (includeCrlId && crlInfo != null) {
certStatusInfo.setCrlId(crlInfo.getCrlId());
}
if (includeArchiveCutoff) {
if (retentionInterval != 0) {
Date date;
// expired certificate remains in status store for ever
if (retentionInterval < 0) {
date = issuer.getNotBefore();
} else {
long nowInMs = System.currentTimeMillis();
long dateInMs = Math.max(issuer.getNotBefore().getTime(),
nowInMs - DAY * retentionInterval);
date = new Date(dateInMs);
}
certStatusInfo.setArchiveCutOff(date);
}
}
return certStatusInfo;
} catch (DataAccessException ex) {
throw new OcspStoreException(ex.getMessage(), ex);
}
} // method getCertStatus
/**
* Borrow Prepared Statement.
* @return the next idle preparedStatement, {@code null} will be returned if no
* PreparedStatement can be created within 5 seconds.
*/
private PreparedStatement borrowPreparedStatement(final String sqlQuery)
throws DataAccessException {
PreparedStatement ps = null;
Connection conn = datasource.getConnection();
if (conn != null) {
ps = datasource.prepareStatement(conn, sqlQuery);
}
if (ps == null) {
throw new DataAccessException("could not create prepared statement for " + sqlQuery);
}
return ps;
}
@Override
public boolean isHealthy() {
if (!isInitialized()) {
return false;
}
if (isInitializationFailed()) {
return false;
}
final String sql = "SELECT ID FROM ISSUER";
try {
PreparedStatement ps = borrowPreparedStatement(sql);
ResultSet rs = null;
try {
rs = ps.executeQuery();
return true;
} finally {
releaseDbResources(ps, rs);
}
} catch (Exception ex) {
LogUtil.error(LOG, ex);
return false;
}
}
private void releaseDbResources(final Statement ps, final ResultSet rs) {
datasource.releaseResources(ps, rs);
}
@Override
public void init(final String conf, final DataSourceWrapper datasource,
final Set<HashAlgoType> certHashAlgos) throws OcspStoreException {
ParamUtil.requireNonNull("conf", conf);
this.datasource = ParamUtil.requireNonNull("datasource", datasource);
sqlCs = datasource.buildSelectFirstSql(1,
"NBEFORE,NAFTER,REV,RR,RT,RIT,PN FROM CERT WHERE IID=? AND SN=?");
sqlCsMap = new HashMap<>();
HashAlgoType[] hashAlgos = new HashAlgoType[]{HashAlgoType.SHA1, HashAlgoType.SHA224,
HashAlgoType.SHA256, HashAlgoType.SHA384, HashAlgoType.SHA512};
for (HashAlgoType hashAlgo : hashAlgos) {
String coreSql = "NBEFORE,NAFTER,ID,REV,RR,RT,RIT,PN," + hashAlgo.getShortName()
+ " FROM CERT INNER JOIN CHASH ON CERT.IID=? AND CERT.SN=? AND CERT.ID=CHASH.CID";
sqlCsMap.put(hashAlgo, datasource.buildSelectFirstSql(1, coreSql));
}
StoreConf storeConf = new StoreConf(conf);
try {
Set<X509Certificate> includeIssuers = null;
Set<X509Certificate> excludeIssuers = null;
if (CollectionUtil.isNonEmpty(storeConf.getCaCertsIncludes())) {
includeIssuers = parseCerts(storeConf.getCaCertsIncludes());
}
if (CollectionUtil.isNonEmpty(storeConf.getCaCertsExcludes())) {
excludeIssuers = parseCerts(storeConf.getCaCertsExcludes());
}
this.issuerFilter = new IssuerFilter(includeIssuers, excludeIssuers);
} catch (CertificateException ex) {
throw new OcspStoreException(ex.getMessage(), ex);
} // end try
initIssuerStore();
if (this.scheduledThreadPoolExecutor != null) {
this.scheduledThreadPoolExecutor.shutdownNow();
}
StoreUpdateService storeUpdateService = new StoreUpdateService();
List<Runnable> scheduledServices = getScheduledServices();
int size = 1;
if (scheduledServices != null) {
size += scheduledServices.size();
}
this.scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(size);
Random random = new Random();
this.scheduledThreadPoolExecutor.scheduleAtFixedRate(storeUpdateService,
60 + random.nextInt(60), 60, TimeUnit.SECONDS);
if (scheduledServices != null) {
for (Runnable service : scheduledServices) {
this.scheduledThreadPoolExecutor.scheduleAtFixedRate(service,
60 + random.nextInt(60), 60, TimeUnit.SECONDS);
}
}
}
@Override
public void shutdown() throws OcspStoreException {
if (scheduledThreadPoolExecutor != null) {
scheduledThreadPoolExecutor.shutdown();
scheduledThreadPoolExecutor = null;
}
}
@Override
public boolean canResolveIssuer(final HashAlgoType hashAlgo, final byte[] issuerNameHash,
final byte[] issuerKeyHash) {
return null != issuerStore.getIssuerForFp(hashAlgo, issuerNameHash, issuerKeyHash);
}
@Override
public X509Certificate getIssuerCert(HashAlgoType hashAlgo, byte[] issuerNameHash,
byte[] issuerKeyHash) {
IssuerEntry issuer = issuerStore.getIssuerForFp(hashAlgo, issuerNameHash, issuerKeyHash);
return (issuer == null) ? null : issuer.getCert();
}
@Override
public Set<IssuerHashNameAndKey> getIssuerHashNameAndKeys() {
return issuerStore.getIssuerHashNameAndKeys();
}
@Override
public CertRevocationInfo getCaRevocationInfo(final HashAlgoType hashAlgo,
final byte[] issuerNameHash, final byte[] issuerKeyHash) {
IssuerEntry issuer = issuerStore.getIssuerForFp(hashAlgo, issuerNameHash, issuerKeyHash);
return (issuer == null) ? null : issuer.getRevocationInfo();
}
protected boolean isInitialized() {
return initialized;
}
protected boolean isInitializationFailed() {
return initializationFailed;
}
private static Set<X509Certificate> parseCerts(final Set<String> certFiles)
throws OcspStoreException {
Set<X509Certificate> certs = new HashSet<>(certFiles.size());
for (String certFile : certFiles) {
try {
certs.add(X509Util.parseCert(certFile));
} catch (CertificateException | IOException ex) {
throw new OcspStoreException("could not parse X.509 certificate from file "
+ certFile + ": " + ex.getMessage(), ex);
}
}
return certs;
}
}