/* * * 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; 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.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; import java.util.concurrent.BlockingDeque; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import org.bouncycastle.cert.ocsp.OCSPResp; import org.bouncycastle.crypto.Digest; import org.bouncycastle.util.encoders.Base64; import org.bouncycastle.util.encoders.Hex; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xipki.commons.common.InvalidConfException; import org.xipki.commons.common.util.LogUtil; import org.xipki.commons.common.util.ParamUtil; import org.xipki.commons.datasource.DataSourceWrapper; import org.xipki.commons.datasource.springframework.dao.DataAccessException; import org.xipki.commons.security.AlgorithmCode; import org.xipki.commons.security.HashAlgoType; import org.xipki.commons.security.util.X509Util; import org.xipki.pki.ocsp.api.IssuerHashNameAndKey; import org.xipki.pki.ocsp.server.impl.OcspRespWithCacheInfo.ResponseCacheInfo; import org.xipki.pki.ocsp.server.impl.store.db.IssuerEntry; import org.xipki.pki.ocsp.server.impl.store.db.IssuerStore; /** * @author Lijun Liao * @since 2.2.0 */ class ResponseCacher { private static final Logger LOG = LoggerFactory.getLogger(ResponseCacher.class); private static final String SQL_ADD_ISSUER = "INSERT INTO ISSUER (ID,S1C,CERT) VALUES (?,?,?)"; private static final String SQL_SELECT_ISSUER_ID = "SELECT ID FROM ISSUER"; private static final String SQL_SELECT_ISSUER = "SELECT ID,CERT FROM ISSUER"; private static final String SQL_DELETE_EXPIRED_RESP = "DELETE FROM OCSP WHERE THIS_UPDATE<?"; private static final String SQL_DELETE_RESP = "DELETE FROM OCSP WHERE ID=?"; private static final String SQL_ADD_RESP = "INSERT INTO OCSP (ID,IID,IDENT," + "THIS_UPDATE,NEXT_UPDATE,RESP) VALUES (?,?,?,?,?,?)"; private final BlockingDeque<Digest> idDigesters; private class IssuerUpdater implements Runnable { @Override public void run() { try { updateCacheStore(); } catch (Throwable th) { LogUtil.error(LOG, th, "error while calling updateCacheStore()"); } } } // class StoreUpdateService private class ExpiredResponsesCleaner implements Runnable { private boolean inProcess; @Override public void run() { if (inProcess) { return; } inProcess = true; long maxThisUpdate = System.currentTimeMillis() / 1000 - validity; try { int num = removeExpiredResponses(maxThisUpdate); LOG.info("removed {} response with thisUpdate < {}", num, maxThisUpdate); } catch (Throwable th) { LogUtil.error(LOG, th, "could not remove expired responses"); } finally { inProcess = false; } } // method run } // class ExpiredResponsesCleaner private final DataSourceWrapper datasource; private final String sqlSelectIssuerCert; private final String sqlSelectOcsp; private final boolean master; private final int validity; private AtomicBoolean onService; private IssuerStore issuerStore; private ScheduledThreadPoolExecutor scheduledThreadPoolExecutor; private ScheduledFuture<?> responseCleaner; private ScheduledFuture<?> issuerUpdater; ResponseCacher(DataSourceWrapper datasource, boolean master, int validity) { this.datasource = ParamUtil.requireNonNull("datasource", datasource); this.master = master; this.validity = ParamUtil.requireMin("validity", validity, 1); this.sqlSelectIssuerCert = datasource.buildSelectFirstSql(1, "CERT FROM ISSUER WHERE ID=?"); this.sqlSelectOcsp = datasource.buildSelectFirstSql(1, "IID,IDENT,THIS_UPDATE,NEXT_UPDATE,RESP FROM OCSP WHERE ID=?"); this.onService = new AtomicBoolean(false); this.idDigesters = new LinkedBlockingDeque<>(); for (int i = 0; i < 20; i++) { Digest md = HashAlgoType.SHA1.createDigest(); idDigesters.addLast(md); } } boolean isOnService() { return onService.get() && issuerStore != null; } void init() { updateCacheStore(); scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(1); scheduledThreadPoolExecutor.setRemoveOnCancelPolicy(true); // check every 600 seconds (10 minutes) this.responseCleaner = scheduledThreadPoolExecutor.scheduleAtFixedRate( new ExpiredResponsesCleaner(), 348, 600, TimeUnit.SECONDS); // check every 600 seconds (10 minutes) this.issuerUpdater = scheduledThreadPoolExecutor.scheduleAtFixedRate( new IssuerUpdater(), 448, 600, TimeUnit.SECONDS); } void shutdown() { if (scheduledThreadPoolExecutor == null) { return; } if (responseCleaner != null) { responseCleaner.cancel(false); responseCleaner = null; } if (issuerUpdater != null) { issuerUpdater.cancel(false); issuerUpdater = null; } scheduledThreadPoolExecutor.shutdown(); while (!scheduledThreadPoolExecutor.isTerminated()) { try { Thread.sleep(100); } catch (InterruptedException ex) { LOG.error("interrupted: {}", ex.getMessage()); } } scheduledThreadPoolExecutor = null; } Integer getIssuerId(HashAlgoType hashAlgo, byte[] nameHash, byte[] keyHash) { IssuerEntry issuer = issuerStore.getIssuerForFp(hashAlgo, nameHash, keyHash); return (issuer == null) ? null : issuer.getId(); } int storeIssuer(X509Certificate issuerCert) throws DataAccessException, CertificateException, InvalidConfException { if (!master) { throw new IllegalStateException("storeIssuer is not permitted in slave mode"); } for (Integer id : issuerStore.getIds()) { if (issuerStore.getIssuerForId(id).getCert().equals(issuerCert)) { return id; } } byte[] encodedCert = issuerCert.getEncoded(); String sha1FpCert = HashAlgoType.SHA1.base64Hash(encodedCert); int maxId = (int) datasource.getMax(null, "ISSUER", "ID"); int id = maxId + 1; final String sql = SQL_ADD_ISSUER; PreparedStatement ps = null; try { ps = prepareStatement(sql); int idx = 1; ps.setInt(idx++, id); ps.setString(idx++, sha1FpCert); ps.setString(idx++, Base64.toBase64String(encodedCert)); ps.execute(); IssuerEntry newInfo = new IssuerEntry(id, issuerCert); issuerStore.addIssuer(newInfo); } catch (SQLException ex) { throw datasource.translate(sql, ex); } finally { datasource.releaseResources(ps, null); } return id; } OcspRespWithCacheInfo getOcspResponse(int issuerId, BigInteger serialNumber, AlgorithmCode sigAlg, AlgorithmCode certHashAlg) throws DataAccessException { final String sql = sqlSelectOcsp; String ident = buildIdent(serialNumber, sigAlg, certHashAlg); Long id = deriveId(issuerId, ident); if (id == null) { return null; } PreparedStatement ps = prepareStatement(sql); ResultSet rs = null; long minNextUpdate = 0; try { ps.setLong(1, id); rs = ps.executeQuery(); if (!rs.next()) { return null; } int dbIid = rs.getInt("IID"); if (dbIid != issuerId) { return null; } String dbIdent = rs.getString("IDENT"); if (!ident.equals(dbIdent)) { return null; } long nextUpdate = rs.getLong("NEXT_UPDATE"); if (nextUpdate != 0) { if (minNextUpdate == 0) { // nextUpdate must be at least in 600 seconds minNextUpdate = System.currentTimeMillis() / 1000 + 600; } if (nextUpdate < minNextUpdate) { return null; } } long thisUpdate = rs.getLong("THIS_UPDATE"); String b64Resp = rs.getString("RESP"); OCSPResp resp; try { resp = new OCSPResp(Base64.decode(b64Resp)); } catch (IOException ex) { LOG.warn("could not parse OCSPResp"); return null; } ResponseCacheInfo cacheInfo = new ResponseCacheInfo(thisUpdate); if (nextUpdate != 0) { cacheInfo.setNextUpdate(nextUpdate); } return new OcspRespWithCacheInfo(resp, cacheInfo); } catch (SQLException ex) { throw datasource.translate(sql, ex); } finally { datasource.releaseResources(ps, rs); } } void storeOcspResponse(int issuerId, BigInteger serialNumber, long thisUpdate, Long nextUpdate, AlgorithmCode sigAlgCode, AlgorithmCode certHashAlgCode, OCSPResp response) { String ident = buildIdent(serialNumber, sigAlgCode, certHashAlgCode); try { byte[] encodedResp; try { encodedResp = response.getEncoded(); } catch (IOException ex) { LogUtil.error(LOG, ex, "could not cache OCSP response iid=" + issuerId + ", ident=" + ident); return; } Long id = deriveId(issuerId, ident); if (id == null) { return; } Connection conn = datasource.getConnection(); try { // delete first String sql = SQL_DELETE_RESP; PreparedStatement ps = datasource.prepareStatement(conn, sql); try { ps.setLong(1, id); ps.executeUpdate(); } catch (SQLException ex) { throw datasource.translate(sql, ex); } finally { datasource.releaseResources(ps, null, false); } sql = SQL_ADD_RESP; ps = datasource.prepareStatement(conn, sql); try { int idx = 1; ps.setLong(idx++, id); ps.setInt(idx++, issuerId); ps.setString(idx++, ident); ps.setLong(idx++, thisUpdate); if (nextUpdate != null && nextUpdate > 0) { ps.setLong(idx++, nextUpdate); } else { ps.setNull(idx++, java.sql.Types.BIGINT); } ps.setString(idx++, Base64.toBase64String(encodedResp)); ps.execute(); } catch (SQLException ex) { throw datasource.translate(sql, ex); } finally { datasource.releaseResources(ps, null, false); } } finally { datasource.returnConnection(conn); } LOG.debug("cached OCSP response iid={}, ident={}", issuerId, ident); } catch (DataAccessException ex) { LogUtil.error(LOG, ex, "could not cache OCSP response iid=" + issuerId + ", ident=" + ident); } } private int removeExpiredResponses(long maxThisUpdate) throws DataAccessException { final String sql = SQL_DELETE_EXPIRED_RESP; PreparedStatement ps = null; try { ps = prepareStatement(sql); ps.setLong(1, maxThisUpdate); return ps.executeUpdate(); } catch (SQLException ex) { throw datasource.translate(sql, ex); } finally { datasource.releaseResources(ps, null); } } private void updateCacheStore() { boolean stillOnService = doUpdateCacheStore(); this.onService.set(stillOnService); if (!stillOnService) { LOG.error("OCSP response cacher is out of service"); } else { LOG.info("OCSP response cacher is on service"); } } /** * update the cache store. * @return whether the ResponseCacher is on service. */ private boolean doUpdateCacheStore() { try { if (this.issuerStore == null) { PreparedStatement ps = null; ResultSet rs = null; try { ps = prepareStatement(SQL_SELECT_ISSUER); rs = ps.executeQuery(); List<IssuerEntry> caInfos = new LinkedList<>(); while (rs.next()) { int id = rs.getInt("ID"); String b64Cert = rs.getString("CERT"); X509Certificate cert = X509Util.parseBase64EncodedCert(b64Cert); IssuerEntry caInfoEntry = new IssuerEntry(id, cert); IssuerHashNameAndKey sha1IssuerHash = caInfoEntry.getIssuerHashNameAndKey(HashAlgoType.SHA1); for (IssuerEntry existingIssuer : caInfos) { if (existingIssuer.matchHash(HashAlgoType.SHA1, sha1IssuerHash.getIssuerNameHash(), sha1IssuerHash.getIssuerKeyHash())) { LOG.error( "found at least two issuers with the same subject and key"); return false; } } caInfos.add(caInfoEntry); } // end while (rs.next()) this.issuerStore = new IssuerStore(caInfos); LOG.info("Updated issuers"); } catch (SQLException ex) { throw datasource.translate(SQL_SELECT_ISSUER, ex); } finally { datasource.releaseResources(ps, rs, false); } return true; } // check for new issuers PreparedStatement ps = null; ResultSet rs = null; Set<Integer> ids = new HashSet<>(); try { ps = prepareStatement(SQL_SELECT_ISSUER_ID); rs = ps.executeQuery(); if (master) { // If in master mode, the issuers are always up-to-date. Here just to check // whether the database is accessible return true; } while (rs.next()) { ids.add(rs.getInt("ID")); } } catch (SQLException ex) { LogUtil.error(LOG, datasource.translate(SQL_SELECT_ISSUER_ID, ex), "could not executing updateCacheStore()"); return false; } catch (Exception ex) { LogUtil.error(LOG, ex, "could not executing updateCacheStore()"); return false; } finally { datasource.releaseResources(ps, rs, false); } // add the new issuers ps = null; rs = null; Set<Integer> currentIds = issuerStore.getIds(); for (Integer id : ids) { if (currentIds.contains(id)) { continue; } try { if (ps == null) { ps = prepareStatement(sqlSelectIssuerCert); } ps.setInt(1, id); rs = ps.executeQuery(); rs.next(); String b64Cert = rs.getString("CERT"); X509Certificate cert = X509Util.parseBase64EncodedCert(b64Cert); IssuerEntry caInfoEntry = new IssuerEntry(id, cert); issuerStore.addIssuer(caInfoEntry); LOG.info("added issuer {}", id); } catch (SQLException ex) { LogUtil.error(LOG, datasource.translate(sqlSelectIssuerCert, ex), "could not executing updateCacheStore()"); return false; } catch (Exception ex) { LogUtil.error(LOG, ex, "could not executing updateCacheStore()"); return false; } finally { datasource.releaseResources(null, rs, false); } } if (ps != null) { datasource.releaseResources(ps, null, false); } } catch (DataAccessException ex) { LogUtil.error(LOG, ex, "could not executing updateCacheStore()"); return false; } catch (CertificateException ex) { // don't set the onService to false. LogUtil.error(LOG, ex, "could not executing updateCacheStore()"); } return true; } // method updateCacheStore private PreparedStatement prepareStatement(String sqlQuery) throws DataAccessException { Connection conn = datasource.getConnection(); try { return datasource.prepareStatement(conn, sqlQuery); } catch (DataAccessException ex) { datasource.returnConnection(conn); throw ex; } } private static String buildIdent(BigInteger serialNumber, AlgorithmCode sigAlg, AlgorithmCode certHashAlg) { byte[] snBytes = serialNumber.toByteArray(); byte[] bytes = new byte[2 + snBytes.length]; bytes[0] = sigAlg.getCode(); bytes[1] = (certHashAlg == null) ? 0 : certHashAlg.getCode(); System.arraycopy(snBytes, 0, bytes, 2, snBytes.length); return Hex.toHexString(snBytes); } private Long deriveId(int issuerId, String ident) { Digest digest = null; try { digest = idDigesters.poll(10, TimeUnit.SECONDS); } catch (InterruptedException ex) { // CHECKSTYLE:SKIP return null; } try { digest.update(intToBytes(issuerId), 0, 2); byte[] bytes = ident.getBytes(); digest.update(bytes, 0, bytes.length); } finally { idDigesters.addLast(digest); } byte[] hash = new byte[20]; digest.doFinal(hash, 0); byte[] hiBytes = new byte[8]; System.arraycopy(hash, 0, hiBytes, 0, 8); BigInteger bi = new BigInteger(1, hiBytes); bi = bi.clearBit(63); return bi.longValue(); } private static byte[] intToBytes(int value) { if (value < 65535) { return new byte[]{(byte) (value >> 8), (byte) value}; } else { throw new IllegalArgumentException("value is too large"); } } }