package org.apereo.cas.adaptors.x509.authentication.revocation.checker;
import com.google.common.base.Throwables;
import org.apereo.cas.adaptors.x509.authentication.CRLFetcher;
import org.apereo.cas.adaptors.x509.authentication.ResourceCRLFetcher;
import org.apereo.cas.adaptors.x509.authentication.handler.support.X509CredentialsAuthenticationHandler;
import org.apereo.cas.adaptors.x509.authentication.revocation.policy.RevocationPolicy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.Resource;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.security.auth.x500.X500Principal;
import java.security.cert.X509CRL;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* CRL-based revocation checker that uses one or more CRL resources to fetch
* local or remote CRL data periodically. CRL resources should be supplied for
* the issuers of all certificates (and intervening certificates for certificate
* chains) that are expected to be presented to {@link X509CredentialsAuthenticationHandler}.
*
* @author Marvin S. Addison
* @since 3.4.7
*/
public class ResourceCRLRevocationChecker extends AbstractCRLRevocationChecker {
private static final int DEFAULT_REFRESH_INTERVAL = 3600;
private static final Logger LOGGER = LoggerFactory.getLogger(ResourceCRLRevocationChecker.class);
/**
* Executor responsible for refreshing CRL data.
*/
private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
/**
* CRL refresh interval in seconds.
*/
private final int refreshInterval;
private final CRLFetcher fetcher;
/**
* Map of CRL issuer to CRL.
*/
private Map<X500Principal, X509CRL> crlIssuerMap = Collections.synchronizedMap(new HashMap<>());
/**
* Resource CRLs.
**/
private final Collection<Resource> resources;
public ResourceCRLRevocationChecker(final boolean checkAll, final RevocationPolicy<Void> unavailableCRLPolicy,
final RevocationPolicy<X509CRL> expiredCRLPolicy, final int refreshInterval,
final CRLFetcher fetcher, final Collection<Resource> resources) {
super(checkAll, unavailableCRLPolicy, expiredCRLPolicy);
this.refreshInterval = refreshInterval;
this.fetcher = fetcher;
this.resources = resources;
}
public ResourceCRLRevocationChecker(final Resource crl,
final RevocationPolicy<Void> unavailableCRLPolicy,
final RevocationPolicy<X509CRL> expiredCRLPolicy) {
this(false, unavailableCRLPolicy, expiredCRLPolicy, DEFAULT_REFRESH_INTERVAL,
new ResourceCRLFetcher(), Collections.singleton(crl));
}
public ResourceCRLRevocationChecker(final Resource[] crl,
final RevocationPolicy<X509CRL> expiredCRLPolicy) {
this(false, null, expiredCRLPolicy, DEFAULT_REFRESH_INTERVAL,
new ResourceCRLFetcher(), Arrays.asList(crl));
}
/**
* Creates a new instance using the specified resource for CRL data.
*
* @param crl Resource containing CRL data. MUST NOT be null.
*/
public ResourceCRLRevocationChecker(final Resource crl) {
this(Collections.singleton(crl));
}
/**
* Creates a new instance using the specified resources for CRL data.
*
* @param crls Resources containing CRL data. MUST NOT be null and MUST have
* at least one non-null element.
*/
public ResourceCRLRevocationChecker(final Collection<Resource> crls) {
this(new ResourceCRLFetcher(), crls, DEFAULT_REFRESH_INTERVAL);
}
public ResourceCRLRevocationChecker(final Resource... crls) {
this(new ResourceCRLFetcher(), Arrays.asList(crls), DEFAULT_REFRESH_INTERVAL);
}
/**
* Instantiates a new Resource cRL revocation checker.
*
* @param fetcher the fetcher
* @param crls the crls
* @param refreshInterval the refresh interval
* @since 4.1
*/
public ResourceCRLRevocationChecker(final CRLFetcher fetcher, final Collection<Resource> crls, final int refreshInterval) {
this(false, null, null, refreshInterval, fetcher, crls);
}
/**
* Initializes the process that periodically fetches CRL data.
*/
@PostConstruct
public void init() {
if (!validateConfiguration()) {
return;
}
try {
// Fetch CRL data synchronously and throw exception to abort if any fail
final Collection<X509CRL> results = this.fetcher.fetch(getResources());
ResourceCRLRevocationChecker.this.addCrls(results);
} catch (final Exception e) {
throw Throwables.propagate(e);
}
// Set up the scheduler to fetch periodically to implement refresh
final Runnable scheduledFetcher = () -> {
try {
final Collection<Resource> resources = getResources();
final Collection<X509CRL> results = getFetcher().fetch(resources);
ResourceCRLRevocationChecker.this.addCrls(results);
} catch (final Exception e) {
LOGGER.debug(e.getMessage(), e);
}
};
try {
this.scheduler.scheduleAtFixedRate(
scheduledFetcher,
this.refreshInterval,
this.refreshInterval,
TimeUnit.SECONDS);
} catch (final Exception e) {
throw Throwables.propagate(e);
}
}
private boolean validateConfiguration() {
if (this.resources == null || this.resources.isEmpty()) {
LOGGER.debug("[{}] is not configured with resources. Skipping configuration...",
this.getClass().getSimpleName());
return false;
}
if (this.fetcher == null) {
LOGGER.debug("[{}] is not configured with a CRL fetcher. Skipping configuration...", getClass().getSimpleName());
return false;
}
if (getExpiredCRLPolicy() == null) {
LOGGER.debug("[{}] is not configured with a CRL expiration policy. Skipping configuration...", getClass().getSimpleName());
return false;
}
if (getUnavailableCRLPolicy() == null) {
LOGGER.debug("[{}] is not configured with a CRL unavailable policy. Skipping configuration...", getClass().getSimpleName());
return false;
}
return true;
}
/**
* Add fetched crls to the map.
*
* @param results the results
*/
private void addCrls(final Collection<X509CRL> results) {
results.forEach(entry -> addCRL(entry.getIssuerX500Principal(), entry));
}
/**
* @return Returns the CRL fetcher component.
*/
protected CRLFetcher getFetcher() {
return this.fetcher;
}
protected Collection<Resource> getResources() {
return this.resources;
}
@Override
protected boolean addCRL(final Object issuer, final X509CRL crl) {
LOGGER.debug("Adding CRL for issuer [{}]", issuer);
this.crlIssuerMap.put((X500Principal) issuer, crl);
return this.crlIssuerMap.containsKey(issuer);
}
@Override
protected Collection<X509CRL> getCRLs(final X509Certificate cert) {
final X500Principal principal = cert.getIssuerX500Principal();
if (this.crlIssuerMap.containsKey(principal)) {
return Collections.singleton(this.crlIssuerMap.get(principal));
}
LOGGER.warn("Could not locate CRL for issuer principal [{}]", principal);
return Collections.emptyList();
}
/**
* Shutdown scheduler.
*/
@PreDestroy
public void shutdown() {
this.scheduler.shutdown();
}
}