CachedCrlRepository.java

/*
 * Java Trust Project.
 * Copyright (C) 2009 FedICT.
 * Copyright (C) 2014-2023 e-Contract.be BV.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License version
 * 3.0 as published by the Free Software Foundation.
 *
 * This software 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, see 
 * http://www.gnu.org/licenses/.
 */

package be.fedict.trust.crl;

import java.lang.ref.SoftReference;
import java.net.URI;
import java.security.cert.X509CRL;
import java.security.cert.X509Certificate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A cached CRL repository implementation. This CRL repository will cache CRLs
 * in memory. This implementation is thread-safe, as far as the passed
 * {@link CrlRepository} is also thread-safe of course.
 * 
 * @author Frank Cornelis
 */
public class CachedCrlRepository implements CrlRepository {

	private static final Logger LOGGER = LoggerFactory.getLogger(CachedCrlRepository.class);

	public static final int DEFAULT_CACHE_AGING_HOURS = 3;

	private final Map<URI, SoftReference<CacheEntry>> crlCache;

	private final CrlRepository crlRepository;

	private int cacheAgingHours;

	private static class CacheEntry {

		private final LocalDateTime timestamp;
		private final X509CRL crl;

		public CacheEntry(X509CRL crl) {
			this.timestamp = LocalDateTime.now();
			this.crl = crl;
		}

		public LocalDateTime getTimestamp() {
			return this.timestamp;
		}

		public X509CRL getCRL() {
			return this.crl;
		}
	}

	/**
	 * Main constructor.
	 * 
	 * @param crlRepository the delegated CRL repository.
	 */
	public CachedCrlRepository(CrlRepository crlRepository) {
		this.crlRepository = crlRepository;
		this.crlCache = Collections.synchronizedMap(new HashMap<>());
		this.cacheAgingHours = DEFAULT_CACHE_AGING_HOURS;
	}

	@Override
	public X509CRL findCrl(URI crlUri, X509Certificate issuerCertificate, Date validationDate) {
		SoftReference<CacheEntry> cacheEntryRef = this.crlCache.get(crlUri);
		if (null == cacheEntryRef) {
			LOGGER.debug("no cache entry ref found: {}", crlUri);
			return refreshCrl(crlUri, issuerCertificate, validationDate);
		}
		CacheEntry cacheEntry = cacheEntryRef.get();
		if (null == cacheEntry) {
			LOGGER.debug("cache entry garbage collected: {}", crlUri);
			return refreshCrl(crlUri, issuerCertificate, validationDate);
		}
		X509CRL crl = cacheEntry.getCRL();
		if (validationDate.after(crl.getNextUpdate())) {
			LOGGER.debug("CRL no longer valid: {}", crlUri);
			LOGGER.debug("validation date: {}", validationDate);
			LOGGER.debug("CRL next update: {}", crl.getNextUpdate());
			return refreshCrl(crlUri, issuerCertificate, validationDate);
		}
		/*
		 * The Belgian PKI the nextUpdate CRL extension indicates 7 days. The actual CRL
		 * refresh rate is every 3 hours. So it's a bit dangerous to only base the CRL
		 * cache refresh strategy on the nextUpdate field as indicated by the CRL.
		 */
		LocalDateTime cacheMaturityDateTime = cacheEntry.getTimestamp().plusHours(this.cacheAgingHours);
		if (validationDate.after(Date.from(cacheMaturityDateTime.atZone(ZoneId.systemDefault()).toInstant()))) {
			LOGGER.debug("refreshing the CRL cache: {}", crlUri);
			return refreshCrl(crlUri, issuerCertificate, validationDate);
		}
		LOGGER.debug("using cached CRL: {}", crlUri);
		return crl;
	}

	private X509CRL refreshCrl(URI crlUri, X509Certificate issuerCertificate, Date validationDate) {
		X509CRL crl = this.crlRepository.findCrl(crlUri, issuerCertificate, validationDate);
		if (null == crl) {
			// we don't want to cache CRL retrieval errors
			this.crlCache.remove(crlUri);
			return null;
		}
		this.crlCache.put(crlUri, new SoftReference<>(new CacheEntry(crl)));
		return crl;
	}

	/**
	 * Evicts a CRL from the CRL cache.
	 *
	 * @param crlUri the CRL URI.
	 */
	public void evictCrlCache(URI crlUri) {
		LOGGER.debug("evict CRL cache: {}", crlUri);
		this.crlCache.remove(crlUri);
	}

	/**
	 * Gives back the CRL cache aging period in hours.
	 *
	 * @return the cache aging in hours.
	 */
	public int getCacheAgingHours() {
		return this.cacheAgingHours;
	}

	/**
	 * Sets the CRL cache aging period in hours.
	 * 
	 * @param cacheAgingHours the CRL cache aging period in hours.
	 */
	public void setCacheAgingHours(int cacheAgingHours) {
		this.cacheAgingHours = cacheAgingHours;
	}
}