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(); } }